同一条存储写三次,账要算三遍:EIP-2200 净计量的结构化价目表 图 1
同一条存储写三次,账要算三遍:EIP-2200 净计量的结构化价目表 · 图 1

合约存储是以太坊最贵的资源:把一个从零开始写的槽位写入非零值,要两万 Gas。于是一个自然的问题出现了——同一个交易里第二次、第三次写同一个槽位,还该收两万吗?全收,等于逼合约把中间值全部堆进内存或外部化;全不收,又白送了一大片状态空间。折中答案叫净计量:按这次写入给状态带来的净变化收费。这条路线几经反复,最终由 EIP-2200 在 2019 年 7 月定稿,随 2020 年初的伊斯坦布尔升级进入主网,状态 Final。

三把尺子量一次写入

规范给每个槽位记三个值:本轮调用开始前存库的原值、此刻的现值、这次指令想写入的新值。账单先看现值和新值是否相等——相等即空写,只收一次读价的量;不等再看原值与现值:如果两者还一致,说明这个槽本轮没人动过,从零写入付两万的置入价,覆盖已有值付五千的重置价,而把值清零除了扣重置价还往退款计数器加一万五。一旦原值和现值不同,槽位就”脏”了:重复弄脏只按读价计,同时规则开始做加减法——先清零后又写非零,先前记的退款被撤销;把脏值改回恰好等于原值,视同还原,退给你置入价或重置价与读价的差额。这套设计的受益名单很具体:同一调用帧里反复读写同一锁槽的重入锁、帧间传递错误码的父子调用,都从每次两千二降到八百量级。

从 EIP-1283 的事故里长出来的护栏

净计量不是新想法。更早的 EIP-1283 提过几乎一样的算法,在测试网跑过一轮,随后被发现一个致命缝隙:简单转账调用默认只给被调方两千三百 Gas 的最低补贴,在旧规则下这点燃料居然够一次存储写,恶意接收合约借此在余额更新到一半时重入提款。2200 的第二条规则就是冲着它焊死的——剩余燃料低于等于补贴线时,存储写直接判定出气失败,不给执行的机会。提案把价目全部换成参数名书写,让后来的调价提案可以只动数字不动规则,这种结构化写法后来成了 Gas 类提案的模板。

与临时存储的分岔

值得一提,同类问题还有另一条解法:给每条交易配一块交易结束就蒸发的临时存储(后来由 EIP-1153 落地)。净计量的哲学是省着花永久状态,临时存储的哲学是干脆不碰永久状态;2200 在理由章节明说自己实现了临时存储想做到的大部分事,代价却小得多——不需要新概念。两条路线后来都进了主网,各管各的场景。

快速问答

问:怎么让存操作的账单尽量便宜? 答:三个方向:少动槽位、同帧复用同槽、交易结束前把临时值改回原值触发还原退款;但别为了退款扭曲业务逻辑,上限受交易总用量占比约束。

问:退款在什么时点生效? 答:交易结算时按比例折进总费,不是指令执行当场返现;调用帧级计数器甚至可能短暂为负,实现细节各客户端有差。

一个直觉账本

拿一把钥匙打比方:把一个槽位从零写成非零,像新租一间仓库,两万押金属门,付一次就完;把旧值盖成新值,像仓库换内容不换租约,五千的钥匙费;把值清零,等于把仓库腾空还给网络,先扣五千再承诺日后退你一万五——因为腾出的空间让全网受益。真正便宜的是同一笔交易里反复折腾同一间仓库:第一次付完租金,第二次起只收八百的开门费;折腾完若把值恢复成交易前的样子,租金还能退。退款计数器就是这套押金的账本,只不过折现比例和结算时点由协议统一规定,不许合约自己挪用。

风险提示:Gas 定价随升级变动,成本估算以当前价目表为准;本文仅为技术科普,不构成投资建议。