该RSKIP(Rootstock改进提案)提出了合约应支付存储租金,以降低存储垃圾的风险并使存储支付更加公平。
RSKIP | 21 |
---|---|
标题 | 高效的持久化存储租金 |
创建日期 | 2016-12-02 |
作者 | SDL |
目的 | Sca |
层级 | 核心 |
复杂度 | 2 |
状态 | 草案 |
本RSKIP提议合约应该支付存储租金,以降低存储垃圾信息的风险,并使存储支付更加公平。与此同时,本RSKIP讨论了存储租金的局限性,因为在某些情况下,额外的复杂性和开销超过了收益。
RSK平台的问题之一是,内存可以以低成本获取,并且永远不会释放,从而迫使所有剩余节点永远存储信息。在现实世界的商业中,几乎没有用户以一次性非经常性付款获得对需要持续维护的财产的永久权利的例子,因此这意味着第三方需要承担定期的维护成本。维护成本很低,但不可忽略,因为持久性数据必须存储在SSD中,以便访问成本与实际成本相匹配。区块链状态存储就是这种情况,成本乘以网络中状态副本的数量。在某些情况下,空间是免费提供的(例如,Google云端硬盘空间),但这是因为空间由Google用户使用的其他服务补贴。此外,不能保证Google会永远提供免费空间。有人可能会说,完整节点是利他的,因此他们愿意承担网络需求的任何存储成本。虽然这在过去可能部分适用于比特币节点,但这种利他行为可能会减少。比特币节点的数量一直在下降,而比特币用户的数量却大大增加,这意味着新用户不愿意像老用户那样运行完整节点。预计区块裁剪和分片技术将使用户能够提交某些部分存储量,而不是整个区块链。但是,验证新区块,而不是历史存储,才是定义完整节点的关键。要验证一个区块,节点需要完整的状态,或者接收所有使用过的状态数据的包含证明。分片因子必须与对等方连接的诚实主机数量成反比,因此如果状态大小增长,并且其他因素保持不变,则本地存储也必须增长。因此,原则上,用户应该为消耗的持久存储支付存储租金(例如,比特币/月)。但是,尚不清楚谁应该支付这笔租金。许多合约都是众筹合约的例子:由大众提供资金和使用的程序,因此它们可能会消耗大量内存,但没有单个用户能够承担租金的负担。无论是在货币努力方面,还是事实上没有单个用户可能有动力执行该任务,无论成本如何。
理想情况下,一个设计良好的众筹合约应该有一种收入生成方法来支付存储租金。例如,每个众筹合约操作都应该附带一笔比特币付款到特殊的租金子账户,众筹合约可以在其中收集所有面向租金的收入。但是,由于大多数众筹合约都是不可变的,因此这种收入收集方法必须在第一天就定义好,并且在该阶段还不清楚收入模式是否可以维持内存租金。最简单的情况是,每个用户独立地为其消耗的内存支付租金。例如,一个类似于DNS的合约会将账户余额附加到每个注册的名称,用户可以自由地发送资金来支付其注册名称的租金。当需要支付租金时,合约会查看哪些名称有足够的余额,删除无法支付租金的名称,从每个名称余额中扣除固定金额,并使用类似于LIFE_EXTEND的操作码支付租金(参见RSKIP08)。但是,这种方法可能效率很低,因为每次支付可能只代表百分之一美分。只要单个的租金支付交易与其他(更重要的)交易捆绑在一起,系统就可以正常工作。但是,不经常使用众筹合约的用户将被迫发送仅以支付租金份额为目的的交易。因此,一种概率方法,即伪随机选择1%的用户在给定时期内为所有用户支付租金,似乎更合适。
参见此处的讨论
每个合约(不是账户)都有四个新字段:rentPreDeposit, rentDue, rentNext, rentEndTimestamp。前三个字段保存 gas 值。最后一个字段是区块号。
租金支付算法如下:
-设 t 为当前日期。
-设 d 为上次向合约发送消息的日期。
-设 s 为持久内存消耗的内存量
-设 payTime 等于4个月的区块数。在此期间,所有者有时间预先支付到期租金,如果预付款高于到期金额,则立即支付到期租金。
-设 graceTime 等于2个月。此期间在 rentEndTimestamp 之后开始。
在 graceTime 结束后,可以使用 HIBERNATE 操作码休眠合约。如果未请求自身或外部休眠,则合约行为正常。
在 payTime 结束后,合约将通过外部休眠或发送给它的任何消息休眠。
下图描述了这些时间间隔:
<img src="../RSKIP21/intervalDiagram_1RSKIP21.png">
然后,当时间 t 的消息或调用到达合约X并需要处理时,执行以下过程。
1. 如果 (t<rentEndTimestamp)
或者 ((d>=rentEndTimestamp) 和 (d<rentEndTimestamp+payTime))
1. r = s*rentCost/(t-d)
2. rentNext = rentNext+r
2. 否则如果 (t>=rentEndTimestamp)
3. r1 = s*rentCost/(rentEndTimestamp-d)
4. rentNext = rentNext + r1
5. rentDue = rentNext
6. rentNext =0.
7. r2 = s*rentCost/(t-rentEndTimestamp)
8. rentNext = rentNext + r2
3. 如果 (t>=rentEndTimestamp+payTime) 那么
9. rentDue = rentDue+nextRent
10. nextRent = 0
4. 休眠合约 X
5. 否则如果 (t>=rentEndTimestamp)
11. 如果 (rentPreDeposit>=rentDue)
1. rentPreDeposit =0
2. rentDue =0
3. rentEndTimestamp = rentEndTimestamp + period
步骤1考虑的情况如下图所示。浅蓝色条表示已支付的租金金额。
<img src="../RSKIP21/case1aRSKIP21.png">
<img src="../RSKIP21/case1bRSKIP21.png">
步骤2考虑的情况如下图所示。
<img src="../RSKIP21/case2RSKIP21.png">
步骤3考虑的情况如下图所示。
<img src="../RSKIP21/case3RSKIP21.png">
在最后一种情况下,存储租金被分成两个金额:第一个金额对应于以浅蓝色绘制并以 rentEndTimestamp 结尾的时间间隔,该时间间隔被添加到先前的支付周期中,第二个金额以黄色绘制对应于之后开始的时间间隔。
必须注意的是,步骤4考虑的情况并非其他情况的排他性,并且可以在输入案例2b、2或3之后输入。
任何合约都可以执行 HIBERNATE <dstcontract>。当 dstcontract 不是自身合约,也不是账户时,会发生以下操作:
设 x 为 rentEndTimestamp 被超过的时间。
如果 x 为负数,则不采取进一步操作
如果 (x>=graceTime) 那么
rentDue = rentDue - rentPreDeposit
rentPreDeposit =0
dstcontract 进入休眠状态
如果休眠成功,HIBERNATE 操作码将返回1,否则返回0。
当 dstcontract 进入休眠状态时,休眠成本设置为零。否则,操作码 HIBERNATE 将支付执行检查的成本,至少等于300个 gas 单位。
每次休眠时,都必须清空预存款,即使它包含的金额超过 rentDue。为了减少应该预先存入的 gas 数量的不确定性,rentDue 在前一个截止日期被“锁定”。
建议截止日期间隔为6个月,因此用户从使用内存的那一刻起最多有6个月的时间支付,并且从他确切知道必须支付多少金额的那一刻起有6个月的时间。
在任何时候,合约都可以使用操作码 DEPOSIT_RENT <gasAmount> 以 gas 为单位进行预存款。
如果为账户调用休眠,则采取以下操作:
如果 (t-lastSendTime>OneYear) 休眠账户,并返回1
否则,返回0
如果合约自行休眠,则无需支付租金,因此合约进入休眠状态,HIBERNATE 返回1。
通常,合约将实现如下方法:
public PayMyRent(int amount){
DEPOSIT_RENT(this,amount)
}
当合约进入休眠状态时,代码、内存和余额都会被哈希处理,并且仅将单个哈希摘要存储在合约地址中。在合约处于休眠状态时,它不能接收或发送付款或消息,除非特殊的 WAKEUP 消息。它可以接收付款到具有相同名称的新账户中,并且当合约被唤醒时,两个余额都会被添加。用户可以通过发送包含完整代码和持久性数据的 WAKEUP 消息来唤醒合约。如果代码和数据不适合一条消息,那么用户需要创建一个代理合约,将代码/数据分块组成,附加这些块,并使用 WAKEUP 操作码向合约发送唤醒 WAKEUP 消息。WAKEUP 操作码有几个参数:代码、代码大小、数据、数据大小。如果数据大小设置为零,但数据指针非零,则将其解释为从中获取代码的存储单元的地址。如果无法唤醒合约,WAKEUP 将返回一个错误代码:代码2表示代码无效,3表示数据无效,4表示租金太低,无法支付重新休眠的成本。
合约存储分为两个子空间。第一个子空间保留休眠状态。此子空间中的所有地址都以0x00开头,后跟256位。第二个子空间以0x01开头,后跟256位。
要选择要写入的子空间,引入了新的 SSPACE 操作码。默认情况下,如果没有执行 SSPACE 操作码,则选择的子空间是0x00。
休眠的成本不取决于内存的大小,因为此 RSKIP 将在新 Trie 结构之上实现(Trie结构中的账户地址下方的持久内存)。因此,不需要计算内存子树的根哈希。
如果调用休眠操作并且它返回1(表示已经发生休眠),则 HIBERNATE 操作码不消耗任何 gas。如果调用 HIBERNATE 但它返回0(未达到休眠状态),则该操作码的成本为300 gas 单位。在内部,首先扣除300 gas 的成本,然后在休眠成功的情况下将其返还。
下表显示了以前的 SSTORE 成本和新的成本:
<table> <tr> <td>标识符</td> <td>之前的成本</td> <td>新的净成本</td> <td>何时支付</td> </tr> <tr> <td>SET_SSTORE </td> <td>20000 </td> <td>2000</td> <td>从 null 到非零</td> </tr> <tr> <td>RESET_SSTORE </td> <td>5000</td> <td>300 或 600</td> <td>从零到零,或从非零到非零。 </td> </tr> <tr> <td>REFUND_SSTORE </td> <td>15000</td> <td>--</td> <td>从非零到零 (将来退还)</td> </tr> <tr> <td>CLEAR_SSTORE </td> <td>5000 </td> <td>50</td> <td>从非零到零 </td> </tr> <tr> <td>CODEBYTE</td> <td>200</td> <td>20</td> <td></td> </tr> </table>
首先,CLEAR_SSTORE 背后的基本原理是,通过将单元格归零,实际上会将其删除,从而节省了空间。但是,要确定单元格是否已被删除,必须首先读取它,并且读取会产生较高的磁盘访问成本。所有当前的 SSTORE 操作都需要先前读取单元格的值,这在大多数情况下是不必要的。读取是阻塞的:在获取地址之前,代码无法继续执行。但是,写入是非阻塞的:写入可以被缓存并在合约结束时执行。
根本问题在于以太坊没有明确说明合约被调用时合约存储是否预先加载,还是按需加载存储单元。预先加载似乎不是正确的方法,因为以优化的方式执行此操作需要将所有合约存储空间压缩成单个连续的磁盘数据块。压缩存储的成本很高,并且该成本不由 SSTORE 支付。
在 RSK 平台中,前提是每次单元格读取都可能需要磁盘访问。为了防止不必要的预读取,所有 SSTORE 操作都消耗恒定的 gas 成本,并且当合约完成时,会根据预先存在的单元格的存在进行一些退款。
<table> <tr> <td>标识符</td> <td>值</td> <td>净成本</td> <td>何时支付</td> </tr> <tr> <td>SSTORE </td> <td>2000</td> <td></td> <td>在执行时</td> </tr> <tr> <td>REFUND_SSTORE_Z_NZ</td> <td>0</td> <td>2000</td> <td>从 null 到非零 (退款)</td> </tr> <tr> <td>REFUND_SSTORE_Z_Z</td> <td>1700</td> <td>300</td> <td>从零到零 (退款)</td> </tr> <tr> <td>REFUND_SSTORE_NZ_NZ</td> <td>1400</td> <td>600</td> <td>从非零到非零 (退款)</td> </tr> <tr> <td>REFUND_SSTORE_NZ_Z</td> <td>1950</td> <td>50</td> <td>从非零到零 (退款)</td> </tr> <tr> <td>SSTORE_YEARLY </td> <td>200</td> <td></td> <td></td> </tr> <tr> <td>CODEBYTE</td> <td>20</td> <td></td> <td></td> </tr> <tr> <td>CODEBYTE_YEARLY </td> <td>20</td> <td></td> <td></td> </tr> </table>
重要的是,实现会检查一个单元格地址是否存在于 trie 结构中,而无需检索其值。RESET_SSTORE 成本被分成两个新的成本:ZERO_ZERO 和 NZERO_NZERO。
修改了 SSTORE 的成本。当 SSTORE 向内存中添加一个值时,会预先扣除空间成本,以及额外的成本。如果稍后将同一单元格设置为零,则空间成本将返回给 gas 池。但是,如果一个单元格是通过不同的交易设置为零的,则不会返还预先扣除的 gas:SSTORE 成本设置为零,但它不会返还 gas。
目前,添加的每个代码字节支付 200 个 gas 单位。这太高了,因为代码会保持在一起,并且可以存储在单个磁盘空间块中。一个包含 32 字节的 SSTORE 数据单元格的价格为 2000 个 gas 单位,但它实际上在内存中大约占用 256 字节(地址+数据+child_pointers_overhead+哈希=~256)。因此,SSTORE 数据的价格为每字节 7 个 gas 单位。因此,CREATE_DATA 将减少 10 倍,降至每字节 20 个 gas 单位。经常性价格将更低:每字节 10 个 gas 单位。
另一个问题是调用合约时加载代码的成本。对于现代操作系统来说,从 SDD/HDD 加载单个字节或 36 KB 的块所花费的时间是相同的,因为数据存储在块中。因此,除非对 CALL 上的程序大小没有限制,否则降低每个字节的代码成本不会打开新的攻击向量。程序大小的问题可以通过几种方式解决:
建立一个上限(就像以太坊所做的那样)
对程序内存进行分页。
第一种方法迫使编译器将一个长程序分割成库,并使用 DELEGATECALL 作为在它们之间调用的手段。这种方法的问题在于,DELEGATECALL 的成本为 700 gas,而普通调用的成本仅为 8 gas。这意味着分割成库必须遵守一个结构规则,即库间调用要远多于跨库调用。
RSK 实现了内存分页。加载合约时,仅加载第一页(最多 8 KB)。不对当前加载的页面之外的跳转进行预先检查,但将作为跳转目标的地址都收集到地址列表中。一个页面不能在数据的中间结束,每个页面都必须以一个有效的指令结束。只有当程序计数器环绕一个页面或当执行指向页面之外的 JUMP 时,才会加载缺少的页面。第一次加载一个页面时,VM 会执行静态检查。它首先扫描代码,将所有目标地址收集到同一个地址列表中,然后扫描找到属于加载页面的地址,并检查每个目标地址是否都有 JUMPDEST 操作码。如果一个地址没有这样的操作码,就会生成一个 out-of-gas 异常。
在 RSK 中,所有静态检查都在创建合约时执行。那时会执行两次检查:首先是初始化代码(包含有效载荷),然后是有效载荷本身(当它返回时)。由于每个代码字节的成本为 20 gas,如果有一个 4M gas 限制,那么最大的代码大小为 200K 字节。
之后,在调用时,可以自由地执行合约,而无需检查。当程序计数器环绕一个页面或当执行指向页面之外的 JUMP 时,会加载缺少的页面,并消耗 300 gas 单位的成本。该页面会保留在内存中直到调用结束。一个 4M 区块 gas 限制允许加载最多 500 个页面,总计 4M 字节的代码。但是,用户必须考虑到,如果 gas 限制降低,可能会阻止调用大型合约。
存储租金的问题之一是它会阻止库的创建。谁来为库支付费用?
简短的回答是,库必须实现一种基于估计每个租金周期 (NC) 的调用次数(在设计时或运行时)的租金收集方法,并将租金成本分摊到对库的前 NC 次调用中。这种内务管理至少会消耗写入存储单元的成本,即 2000 gas 单位,因此仅适用于租金份额 > 2000(或 100 字节的代码)的库。例如,一个包含 100 KB 的库,每年至少使用 100 次,每次调用的价格为每个首次 100 次调用 50 gas 单位。
如果一个库进入休眠状态,这对去中心化用户来说非常不方便,因为他可能不知道库失败的确切原因,或者不知道哪些其他调用的库不再可用。整个库调用库的想法,其中系统的活性取决于收到的调用速率,至少可以说看起来很不可靠。
一种可能性是为库创建一个特殊的实体。库没有存储或余额,并且每个字节的成本为 1000(比以太坊高 5 倍,比 RSK 的初始字节成本高 50 倍)。如果有一个 4M gas 限制,则库的最大大小为 4 KB。
同样的方法可以用于支持永恒合约,方法是将 SSTORAGE 成本增加到 50K(比 RSK 的初始成本高 25 倍)。
要唤醒一个合约,用户必须提供 spv 路径和所需的哈希预映像。在交易中传输这些数据的成本为每字节 x 。WAKE UP 操作码本身也有成本,其中包括以下成本:
代码字节成本(每字节 20 gas 单位)
存储单元成本(每个单元格 2000 单位)
恢复余额和其他合约内部字段的固定成本(2000 单位)
此外,租金考虑的内存始终具有 64 字节的固定初始成本,即 128 个 gas 单位。
要了解休眠如何在某些情况下省钱,请想象一个具有以下属性的合约:
代码大小:1024 字节
存储大小:4 个单元格
要传输的总字节数(不包括单元格地址):~640 字节
该合约被编程为在每次操作后进行自我休眠。这意味着 rentCost 始终为零。维持该合约活跃 1 年的成本为:10242+8256*2+128=6272 gas 单位。
唤醒的成本为 201024+42000+2000=30480。
传输每个非零字节的成本为 68 gas 单位。因此,在每个交易中传输 640 字节的成本为 43520。显然,除非该合约将不活跃 12 年,否则这样做没有好处。
此外,很明显,不支付的代价是应支付金额的 12 倍,这似乎有些夸张。
如果可以以更低的成本传输数据,自我休眠可以省钱。一种这样的方法是临时 segwit 数据,合约只能在包含后的两个区块中使用它。区块将分两个阶段传输,第一个阶段不包含 segwit,第二个阶段仅包含 segwit 数据。这允许矿工在接收 segwit 数据时验证区块。如果验证需要 1 秒,则可以通过这一秒无成本地接收数据。该数据的成本将是临时区块链存储的成本。如果该数据保存在 RAM 缓存中,则每个字节的成本可以低至 1 gas 单位。
这意味着一个具有 4M gas 限制的区块可以包含 4M 字节的数据,并且数据必须在 1 秒内传输,这超出了链接的正常容量。因此,我们必须设置更高的价格:例如,每字节 5 gas 单位。
一个 gas 单位的成本为 1.4*10^-7 美元。
当我们计算存储成本时,我们必须乘以 1000(newtok 中所需的节点数)。
那么,HDD 中每个字节的永久区块链存储成本为 2.4 10^-11。乘以 1K 个节点为 2.4 10^-8(少 7 倍)
SDD 中永久存储的成本为 5.5 10^-11。乘以 1K 个节点为 5.5 10^-8(少 3 倍)
在 RAM 缓存中存储的成本为 1.6 10^-10(持续 4 年)。如果它只在缓存中存在 1 天,则成本为 1.6 /1460 10^-10 为 1.0910^-13。乘以 1K 个节点为 1.0910^-10(少 1000 倍)。
因此,存储成本远低于在 segwit 中传输的成本。占主导地位的成本始终是磁盘访问的成本:如果必须从磁盘检索合约数据,则访问需要 1 毫秒,那么对于一个 4M gas 区块限制,它应该花费 4000 gas 单位。显然,segwit 数据必须保存在内存中,直到它被使用或丢弃。它可以写入磁盘,但这不能阻止挖矿和交易处理。
似乎 segwit 系统可以用于恢复状态,但是它需要两个交易而不是一个:第一个交易提供 segwit 数据(并支付它),第二个交易使用 segwit 数据执行唤醒命令。因为每个交易的成本为 21K gas,所以整个过程的成本增加了 21K gas,仅仅是为了节省 6272 gas 单位!
一种降低第二笔交易成本的方法是允许预付交易费用。这可以通过在交易中包含以下交易的哈希来实现。
因此,如果这些哈希可以在内存中保存一段时间,则后续交易可以非常便宜。
添加此功能很复杂,除非我们更改签名系统,以便合约可以验证它。因此,我们可以更改与第一个交易的合约,以接受第二个或更多交易,而无需签名(仅通过其哈希)。但是签名不会包含 gasPrice,因为 gasPrice 可能会因区块而异。交易还需要一个递增的序列号。
显然,所有这些复杂的东西都非常有趣,但毫无用处。
只有在以下情况下,此设计才是安全的:
始终有一个合理的 minGasPrice。任何合约都不能支付低于此价格的费用。这是为了保护矿工以免无成本购买永久内存。
为了防止提供内存的服务避免租金,内存成本不应高于将其从一个合约传输到另一个合约的成本。
由于租金时间是通过区块号来衡量的,如果区块间隔没有遭受高且持续的偏差,那么成本仍然是可以接受的。
设 m 为合约以 32 字节字为单位持久化的内存量。
参数:<address>
GasCost:由调用者提供(而不是从休眠存款中提取)
返回:error_code
参数:contract_address spv_path_address spv_path_length code_address code_size trie_address trie_size
返回:error_code
参数:<address>
值:要存入的金额。它必须等于或高于 m*f
接受的值:m*f
参数:<space>
更改内存空间。
space 的可能值:{ 0,1}。
以下 SSPACE 之后的所有 SLOAD 和 SSTORE 操作都适用于所选空间。
返回存款中的 gas 量。
返回下一个截止日期必须支付的 gas 量。
返回正在累积的气体量,以便在下一个截止日期添加到到期租金中。
参数:<gasIndex>
返回:gasIndex(例如操作码)的成本。
返回:持久内存的大小。
由于所需的操作码数量,最好通过一个或多个本机合约来实现它们。在这种情况下,必须评估调用的成本是否超过操作的期望成本。
没有所有者的合约必须确保与合约交互的用户支付租金。实现此目的的一种方法是跟踪每个用户进行部分租金支付的时间,并检测自上次支付以来是否已经过了太长时间。合约能够通过操作码 GASDEPOSITED 知道存款中有多少 gas,并且它们也可以通过 SMEMSIZE 操作码知道有多少内存被持有。
以下伪代码从每次方法调用中收集租金份额:
SMEMSIZE (成本 2)
BLOCK.TIMESTAMP (成本 2)
SLOAD LastCallTimeStamp (成本 200)
SUB (成本 3)
MUL (成本 5)
DEPOSIT (成本 5)
BLOCK.TIMESTAMP (成本 2)
SSTORE LastCallTimeStamp (以太坊中为 20K,RSK 中为 2K)
在 RSK 中执行此代码段的成本为:2220 单位。
这相当于存储:
<table> <tr> <td>大小</td> <td>2200 个 gas 单位的等效租金时间成本</td> <td>合约类型</td> <td>预计使用频率</td> </tr> <tr> <td>1.1 千字节</td> <td>1 年</td> <td>非常简单的钱包</td> <td>每月 1 次</td> </tr> <tr> <td>11 千字节</td> <td>36 天</td> <td>多重签名钱包 (不考虑代码重用)</td> <td>每 10 天 1 次</td> </tr> <tr> <td>110 千字节</td> <td>3.6 天</td> <td>DAO</td> <td>每天 1 次</td> </tr> <tr> <td>1 兆字节 </td> <td>8.6 小时</td> <td>适用于 3500 个用户的注册表</td> <td>每小时 1 次</td> </tr> <tr> <td>10 兆字节</td> <td>51 分钟</td> <td>适用于 3.5 万用户的注册表 </td> <td>每分钟 1 次</td> </tr> <tr> <td>100 兆字节 </td> <td>5.1 分钟</td> <td>适用于 39 万用户的加密资产</td> <td>每 10 秒 1 次</td> </tr> <tr> <td>1 GB</td> <td>30 秒</td> <td>适用于 390 万用户的加密资产</td> <td>每秒 1 次</td> </tr> </table>
该表显示,每次传入付款时执行用于支付租金的代码段的成本比支付的金额高一个数量级。有两种解决方案:要么将 rentCost 增加 10 倍,要么平台自动提供值 LastCallWriteTimeStamp,该值指示导致合约状态发生更改的上次调用合约的时间。平台会将此值与更改的账户一起存储,因此更新它没有任何成本。此外,平台将提供对标志 contractModified 的访问权限,如果合约以任何方式被修改(余额、内存等),则该标志变为 true。因此,此代码段将添加到每次合约调用的末尾:
ContractModified (成本 2)
JUMPI exit (成本 10)
SMEMSIZE (成本 2)
BLOCK.TIMESTAMP (成本 2)
LastCallWriteTimeStamp (成本 2)
SUB (成本 3)
MUL (成本 5)
DEPOSIT (成本 5)
BLOCK.TIMESTAMP (成本 2)
exit:
JUMPDEST (成本 1)
总成本为 33。 <table> <tr> <td>近似大小</td> <td>相当于 33 个 gas 单位的租金时间成本</td> <td>合约类型</td> <td>预期使用频率</td> </tr> <tr> <td>165 字节</td> <td>1 个月</td> <td>具有嵌入式匿名规则的微型钱包</td> <td>每月 1 次</td> </tr> <tr> <td>1.65 Kbyte</td> <td>与预期相同</td> <td>具有速率限制的非常简单的钱包</td> <td>每月 1 次</td> </tr> <tr> <td>16.5 Kbyte</td> <td>与预期相同</td> <td>多重签名钱包</td> <td>每 10 天 1 次</td> </tr> <tr> <td>165 Kbyte</td> <td>与预期相同</td> <td>DAO</td> <td>每天 1 次</td> </tr> <tr> <td>1.6 Mbyte</td> <td>与预期相同</td> <td>3500 个用户的注册表</td> <td>每小时 1 次</td> </tr> <tr> <td>16 Mbytes</td> <td>与预期相同</td> <td>3.5 万用户的注册表</td> <td>每分钟 1 次</td> </tr> <tr> <td>165 MBytes</td> <td>与预期相同</td> <td>39 万用户的加密资产</td> <td>每 10 秒 1 次</td> </tr> <tr> <td>1.6 GB</td> <td>与预期相同</td> <td>390 万用户的加密资产</td> <td>每秒 1 次</td> </tr> </table>
通过这种定价,支付部分租金的成本与支付的租金成本相似。
这是一个 ASSET 合约,其中子合约用于增加并行化因子。ASSET 合约不会在其自己的持久性内存中存储任何内容,但仍然需要支付维护 ASSET 合约存活的成本。
每个子合约都将使用地址 HASH(父地址 | 用户地址)创建。每个子合约都可以具有以下方法:
public payRent()
{
if (gas<rentGas) thow;
DEPOSIT_RENT(this,rentGas*5) // pays for code + data 支付代码+数据的费用
DEPOSIT_RENT(parent,rentGas) // overpays for some constant code/data 为一些恒定代码/数据多付了费用
}
另一种选择是父合约实现以下方法:
public payRent() {
address a = msg.sender;
address childContract = SHA3( this.address , a);
DEPOSIT_RENT(childContract,rentGas);
DEPOSIT_RENT(this,rentGas);
}
CALL 的成本是否应考虑将所有持久合约数据恢复到 RAM 中的成本?为了回答这个问题,需要分析合约如何使用存储以及系统如何缓存合约存储。一方面,CALL 的成本目前是固定的,因此无法考虑将所有合约存储从磁盘恢复到 RAM 中。另一方面,为从内存中读取的每 32 个字节支付一次磁盘访问费用是过度的。对帐户和合约存储使用统一树意味着存储元素存储在磁盘上的随机位置,因此无法以较低的成本同时检索所有元素。但是,存在存储缓存,因此在同一事务中第一次访问后,后续访问的执行速度会快得多。写入或读取存储单元一百次的成本与访问它一次的成本相同。如果 gas 中的访问成不变,那么合约应使用易失性内存作为缓存来执行多个操作,直到最后仅对每个访问的单元进行一次 SSTORE 操作。根据 SSTORE/SLOAD 的第一次与否更改其成本似乎过于复杂,并且会阻止静态代码分析器轻松推断代码块的 gas 成本。
我们得出结论,CALL 应该具有固定的成本,并且该成本不应考虑合约内存检索。每个存储单元访问都应考虑磁盘访问的成本。
为了找到 SSTORE、CREATE 和 CALL 的合适价格,我们必须首先分析这些操作码的价格,因为处理这些操作码涉及的不仅仅是从磁盘读取。
由于存储成本必须定期支付,因此可以降低初始存储获取成本。我们假设 trie 中的每个条目在内存中占用 128 字节(32 字节密钥、32 字节数据和 64 字节开销),在磁盘中占用相同的量,并且我们将尝试计算网络存储此值的实际成本,以及检索每个值的成本。
稍后将使用的一些成本:
<table> <tr> <td>媒介</td> <td>成本</td> <td>1 TB 成本/年 (4 年摊销)</td> <td>1 字节的成本</td> </tr> <tr> <td>HDD</td> <td>内部 50 美元/1 TB</td> <td>12.5 美元</td> <td>12.5 10^-12</td> </tr> <tr> <td>SDD</td> <td>内部 70 美元/240 GB</td> <td>72 美元</td> <td>72 10^-12</td> </tr> <tr> <td>RAM</td> <td>50 美元/8 GB</td> <td>1600 美元</td> <td>1600 10^-12 = 1.6 10^-9 </td> </tr> </table>
我们想将这些成本与当前 RSK/Ethereum 的存储成本进行比较。
操作码 SSTORE 的成本为 20K gas(但在删除数据时会退还 15K)。因此净成本为 5K gas。假设平均 gas 价格为 20 gwei,并且 7 ETH/USD,那么将数据持久化到合约存储中的成本为 7^10^-4。
下表比较了将相同内容存储在不同类型的存储中的成本。该表考虑了由于磁盘和计算机出现故障或过时而必须定期更换它们。摩尔定律在过去运行良好,它说计算能力每 18 个月翻一番。目前,这种增长已经放缓到每 30 个月翻一番 [https://en.wikipedia.org/wiki/Moore's_law]。
磁盘存储密度(克莱德定律)存在类似的定律,密度每年增加 40%,但这一预测在过去 5 年中未能实现。实际的改进约为每年 15% [https://en.wikipedia.org/wiki/Mark_Kryder]。
没有精确衡量计算机平均寿命的方法(并且它因是笔记本电脑、台式 PC 还是平板电脑而异)。但我们可以安全地假设平均为 5 年。因此,按照目前 HDD 存储密度的趋势,5 年后,新硬盘驱动器的容量将以相同的价格翻一番。这意味着每字节的价格减半。如果价格每 5 年保持减半,这意味着永久存储一个字节的成本仅为存储一个字节 5 年成本的两倍。
对于 SSD 磁盘和 RAM,控制内存密度增加的定律是摩尔定律,因此每 2.5 年(而不是 5 年),我们看到内存大小翻了一番。在下表中,我们非常保守地假设 SSD 和 RAM 的每字节价格每 5 年下降一次。
<table> <tr> <td>媒介</td> <td>成本 [美元]</td> <td>5 年的 1 字节成本</td> <td>永远的 1 字节成本</td> <td>比率</td> <td>考虑 1K 复制的比率</td> </tr> <tr> <td>ETH SSTORE</td> <td>7^10^-4</td> <td></td> <td></td> <td>1</td> <td></td> </tr> <tr> <td>HDD</td> <td>1.6 10 ^-9</td> <td>12.5 10^-12</td> <td>25 10^-12</td> <td>218750</td> <td>218</td> </tr> <tr> <td>SDD</td> <td>9.2 10^-9</td> <td>72 10^-12</td> <td>144 10^-12</td> <td>38043</td> <td>38</td> </tr> <tr> <td>RAM</td> <td>2.0 10^-7</td> <td>1.6 10^-9 </td> <td>3.2 * 10^-9</td> <td>1750</td> <td>1.75</td> </tr> </table>
我们无法从这些数字中得出明确的结论,但我们可以找到几种可能的解释。
SSTORE 的价格与 RAM 中的存储有关
以太坊的设计是为了拥有 1 万个完整节点,而不是 1 千个。
有 10 倍的安全边际。
定价的不是存储。SSTORE 价格考虑了另外两个资源:访问时间和新节点下载状态的成本。
以太坊当前区块 gas 限制为 4M gas。gas 限制在短期内有两个用途:限制区块大小,并限制区块所需的计算量。由于在 RSK VM 中执行算术循环平均每条指令需要 200 纳秒(每秒 5M 条指令),并且假设所有其他操作码成本都根据执行它们所需的实际时间设置,我们可以粗略地估计 topping 4M gas 的区块需要 800 毫秒才能执行(因为 4M/5M=0.8)。假设我们希望将区块执行时间限制在一秒钟内,则每个存储操作的 gas 成本不得超过它所花费的时间的成本。下表显示了典型的存储访问时间。
<table> <tr> <td>媒介</td> <td></td> <td>每秒最大操作数</td> <td>相对于其消耗的时间的最小 gas 成本</td> <td>成本 [美元]</td> </tr> <tr> <td>HDD</td> <td>10 毫秒</td> <td>100</td> <td>40K</td> <td>~2 美分</td> </tr> <tr> <td>SDD</td> <td>0.1 毫秒</td> <td>10K</td> <td>400</td> <td>0.02 美分</td> </tr> <tr> <td>RAM</td> <td>10 纳秒</td> <td>10^8</td> <td>~0</td> <td>~0</td> </tr> </table>
由于以太坊 SSTORE 的成本为 5K,我们可以推断出以太坊的设计是为 SSD 磁盘量身定制的。数据 RAM 缓存的存在(以及目前以太坊状态数据可以容纳在 RAM 中)意味着矿工可以使用带有 HDD 的计算机,而不会有受到攻击的风险,直到状态超过 RAM 大小或进程空间。但是,按照目前以太坊状态增长的速度,这个限制可能会在一年内达到。但是,由于通过垃圾邮件攻击状态仍然需要攻击者填充状态,并且以目前的存储初始成本(20K 兑换 128 字节),填充 4 GB RAM 的成本为 33.5 万美元。我们得出结论,在未来几年内,强烈建议以太坊矿工不要使用 HDD 磁盘。
假设这对于 RSK 也是如此,上面的图表向我们表明,任何合约存储操作的价格都不应低于 400 个 gas 单位。新的 RSK gas 价格为每个非零数据的 SSTORE 2K 验证了这个下限。
区块链是一种不断增长的数据结构,任何事物都无法改变这一点。在比特币的最初几年,第一次连接到网络的体验非常顺畅,整个区块链在几个小时内就下载完毕了。为了缩短初始下载时间,必须进行一些改进和优化:例如,比特币在嵌入在代码中的最后一个检查点之前不会验证签名。以太坊选择了一种更激进的方法,新节点可以以快速模式启动。在这种模式下,他们可以收到区块链中最后一个区块的“签名”或受信任的检查点,他们可以在其中找到当前状态的哈希摘要。然后他们可以从对等方下载状态,并根据哈希摘要验证状态。在以太坊节点 Parity 中,允许从对等文件共享网络下载状态的完整快照并导入它。截至 2016 年 8 月,导入过程仅需 5 分钟。因此,设置完整节点的唯一真正限制是状态的下载时间。 [https://github.com/ethcore/parity/releases/tag/v1.3.0]
此外,trie 快照可以被压缩,占用比在 RAM(实时对象)或磁盘上的索引数据库中小得多的空间。截至 2016 年 8 月,parity 快照占用 150 Mb。使用 5 Mbps 链接下载 150 Mb 需要 50 分钟。[https://en.wikipedia.org/wiki/List_of_countries_by_Internet_connection_speeds]。新用户愿意等待同步的时间是一个主观问题,并且取决于许多其他因素。但是,如果状态大小的增长速度低于平均带宽的增长速度,那么启动节点将始终花费相同的时间。状态的大小应与用户数量和用户参与的活跃服务数量相关。两者最终都可以达到最大值。但是,这个最大值可能非常高。在 2016 年第三季度,Paypal 拥有超过 1.92 亿个活跃帐户 [https://www.statista.com/statistics/218493/paypals-total-active-registered-accounts-from-2010]。Paypal 花了 16 年才达到这个用户量。因此,我们可以预测,像 RSK 这样的区块链有可能在十年内获得 2.5 亿个用户。
我们可以假设应用程序将标准化,因此 2.5 亿用户并不意味着 2.5 亿个钱包合约代码副本,而是由所有用户调用的单个副本,我们将分析重点放在合约存储上。我们假设每个用户使用 8 个应用程序(例如加密资产),并且每个应用程序消耗 256 字节,因此每个用户消耗 2 Kbytes。如果区块链的目标是 2.5 亿用户,那么状态将高达 500 GB。今天传输 500 GB 需要 115 天。尼尔森定律 说,用户可用的带宽每年增加 50%,因此十年后的平均带宽将是今天的 57 倍。这意味着下载状态仍然需要 2 天。
似乎即使在非常乐观的增长情景下,完整节点也能够应对区块链的重量。但是,我们无法预测矿工会正确地设置每个 gas 单位的最低价格,以便用户不会发送垃圾邮件给状态。事实上,他们有动机降低最低 gas 价格,既可以通过侧信道获得费用,又可以通过补贴区块链的使用来使其更受欢迎。他们这样做可能是正确的!
存储租金的实施不是为了防止垃圾邮件,而是为了保护未来区块链用户免受矿工可能采取的某些时期内以增加用户采用率和超越竞争对手的市场驱动措施的影响。如果没有存储租金,这些市场驱动的决策就会变成民粹主义:为短期利益牺牲资源,并阻止长期成功。存储租金还可以保护区块链免受技术和区块链采用率的错误计算和预测的影响。但是,实施存储租金很复杂,因为许多租金支付都是微交易,因此实施的系统必须确保租金收集成本不会给扩展带来新的限制因素。
如前所述,一个区块应在不到 1 秒的时间内处理完毕。假设硬盘技术(而不是 SSD),磁盘访问大约需要 10 毫秒,这意味着一个区块中只能有 400 个磁盘操作。处理一个简单的帐户到帐户事务至少需要 2 次磁盘访问,因此使用硬盘驱动器设置了 20 tps 的上限。由于我们预计 RSK 在第一年不会达到 20 tps,因此这个数字可以用来启动。
一些专家估计,在未来 4 年内,SSD 可能会完全取代 1GB 以下空间市场的 HDD。此外,由于状态可以在 RAM 中缓存(如果状态适合),因此在第一年内,我们在区块执行期间不需要外部存储。
即使没有实施存储租金,RSK 也可以设置 10K 的初始 SSTORE 价格,平稳地降低,以便在 4 年后,随着 SSD 驱动器超过 HDD,它变得接近 ~400。
SSD 的访问时间约为 0.1 毫秒,因此使用相同的推理(4M gas 限制 / 10K 操作 = 400),SSTORE 的 gas 成本应低至 400 个单位,非常接近以太坊中单个磁盘访问的当前值(例如 BALANCE 操作码所需的)。
CALL 操作的 gas 成本为 700 个单位,与 SLOAD 的成本一致。因此,以太坊似乎假定状态存储在 SSD 中。
一个标准的 SSD 可以存储 512 Gbytes,允许最多 20 亿个简单帐户(每个 256 字节),或 1 亿个多重签名钱包合约,因此似乎有足够的空间在全球范围内增长。
目前,以太坊大约有 60 万个活跃帐户(空帐户正在被修剪)。60 万个帐户也适合 RAM 内存(大约 150 Mbytes)。
标准机器 RAM 大小为 8 GBytes。假设 6 Gigabyte 留给操作系统和其他应用程序,则 2 GBytes 可以由完整节点使用。该空间可以存储 8M 个简单帐户,或 39 万个多重签名钱包,这对于全球覆盖而言似乎很低。作为比较,比特币目前有 44M 个 UTXO(在磁盘上压缩后占用超过 1.3 Gb)。
为了能够选择合适的 SSTORE 初始价格和经常性价格,我们必须假设外部conditions:
在最初的 4 年中,将允许拥有 HDD 的计算机。这些计算机将需要将状态存储在 RAM 中。因此,在最初的 4 年中,帐户数量不能超过 16M。
之后,所有状态都将存储在 SSD 中,并且不允许使用拥有 HDD 的计算机。
作为结论,SSTORE 经常性价格可以放心地降低 100 倍(200 gas),而初始价格仅降低 10 倍(2000 gas)。
因此,租金成本(每字节成本)设置为每年 2 个 gas 单位,并且每个存储单元计为 256 字节。因此 rentCost = 2/365/86400(每天的秒数)。
即使初始成本可以进一步降低,最好还是保守一点,因为与写入操作相关的许多隐藏成本,例如:
大多数类型的内存写入比读取消耗更多的能量。
写入涉及使缓存无效和合并缓存,而读取则不是。
写入可能会干扰事务序列化,而只有读取不会。
版权和相关权利通过 CC0 放弃。
- 原文链接: github.com/rsksmart/RSKI...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!