引介 | GasToken:我为何不再担心 gas 价格飙升(上)
本文旨在探索 EVM 的 gas 机制,尤其是 GasToken 的 EVM gas 机制。首先,为了降低技术理解的难度,本文需要先给出一段介绍。如果你不想了解底层机制,可以直接跳到后文的 “具体实现细节” 一节开始阅读。
- 要是某个表出现反转那就完蛋了 —— 但是 EVM 就不一样了 -
引言—— gas 的基础知识
以太坊使用了一种 gas 计量系统,主要是为了防止停机问题和重入攻击(reentry attack)。这个计量系统似乎是最简单,也是最健壮的(尽管还有其它计量系统,如 EOS 系统)。EVM 的每个操作码都有固定的 gas 消耗量,黄皮书中注明了不同指令的 gas 成本等级:零级(0 gas)、基础级(2 gas)、超低级(3 gas)、低级(5 gas)、高级(10 gas),以及规则更加复杂的特殊等级。
例如,在 EVM 堆栈上添加或删除操作需要花费 3 gas。其中一些操作码在某些硬分叉部署之后经过了重新定价,例如,calldata(EVM 中的只读内存区域 —— 总计的 4 种存储类型之一)已经从每字节 68 gas 的价格下调至每字节 18 gas。重新定价似乎是为了促进二层可扩展方案的实现,因为二层可扩展方案需要链上数据可用性。还有证据表明,操作码的原始定价并没有经过充分分析,依然存在定价不当的问题。另外,更改操作码的 gas 消耗量也会带来问题:
降低指令的 gas 价格可能会让重入攻击变得可行 提高指令的 gas 价格可能会致使调用失败,因为这会导致 gas 分配量不足以执行调用
区块的 gas 上限
接下来进入正题
SSTORE
、 CREATE
、 CREATE2
和 SELFDESTRUCT
。这些操作码的共同点是,它们都涉及状态,因此也涉及硬盘读写(以太坊网络的节点通常使用固态硬盘)。这些操作码成本更高,因为它们会影响永久存储和全局状态树。什么是 GasToken
清理/自毁合约:- 24,000 gas 清理/删除存储:-15,000 gas
approve
和 transferFrom
操作码,可以称为多步骤交易的一部分。最初,GasToken 有两种变体,分别采用不同的设计:GST1 和 GST2。GST1 使用的是存储成本和退款机制,GST2 使用的是 CREATE
和自毁机制。这些变体采取不同的节约方案,具体取决于 gas 价格差值比(铸造代币和释放代币时的 gas 价格差值比)。由于 gas 价格率更高,GST2 更能节约 gas。具体实现细节
GST1 —— 基于存储
mint()
函数:function mint(uint256 value) public {
uint256 storage_location_array = STORAGE_LOCATION_ARRAY; // can't use constants inside assembly
if (value == 0) {
return;
}
// Read supply
uint256 supply;
assembly {
supply := sload(storage_location_array)
}
// Set memory locations in interval [l, r]
uint256 l = storage_location_array + supply + 1;
uint256 r = storage_location_array + supply + value;
assert(r >= l);
for (uint256 i = l; i <= r; i++) {
assembly {
sstore(i, 1)
}
}
// Write updated supply & balance
assembly {
sstore(storage_location_array, add(supply, value))
}
s_balances[msg.sender] += value;
}
简单来说,我们使用一个存储起点常量来标记 EVM 存储的开始,而且这个常量还包括我们已经写入多少个插槽的值。如果你想了解更多关于 EVM 中永久存储布局的内容,请阅读这篇文章。通过第 12 和第 13 行的代码,我们可以计算出新的待写入插槽范围,并在第 17 行的 for 循环中使用 SSTORE
操作码来将数据写入这些插槽,存储数值 1(这个值可以替换成任何非零值)。然后,我们在第 22 和 24 行代码处更新已写入数据的插槽数量和余额。
freeFromUpTo(uint value)
、 freeFrom(uint value)
、 freeUpTo(uint value)
和 free(uint value)
。这类函数在下文统称为 free*()
函数,调用内部函数 freeStorage()
:function freeStorage(uint256 value) internal {
uint256 storage_location_array = STORAGE_LOCATION_ARRAY; // can't use constants inside assembly
// Read supply
uint256 supply;
assembly {
supply := sload(storage_location_array)
}
// Clear memory locations in interval [l, r]
uint256 l = storage_location_array + supply - value + 1;
uint256 r = storage_location_array + supply;
for (uint256 i = l; i <= r; i++) {
assembly {
sstore(i, 0)
}
}
// Write updated supply
assembly {
sstore(storage_location_array, sub(supply, value))
}
}
如你所见,该函数与上文讨论的 mint()
函数几乎相同,主要的区别在于第 13 行代码,将值 0 写入存储会导致 EVM 释放存储插槽。这行代码会触发 gas 退款,让 gas 退款计数器增加 15000。更新 ERC-20 类型余额的任务也由 free*()
函数承担。
GST2 —— 基于合约
mint()
函数等价的函数,在 GST2 合约里叫做 makeChild()
,它是一个内部函数,使用 EVM 来汇编创建一个简单的 “child” 合约,而且该合约只能用 “parent” 合约来摧毁:function makeChild() internal returns (address addr) {
assembly {
// EVM assembler of runtime portion of child contract:
// ;; Pseudocode: if (msg.sender != 0x0000000000b3f879cb30fe243b4dfee438691c04) { throw; }
// ;; selfdestruct(msg.sender)
// PUSH15 0xb3f879cb30fe243b4dfee438691c04 ;; hardcoded address of this contract
// CALLER
// XOR
// PC
// JUMPI
// CALLER
// SELFDESTRUCT
// Or in binary: 6eb3f879cb30fe243b4dfee438691c043318585733ff
// Since the binary is so short (22 bytes), we can get away
// with a very simple initcode:
// PUSH22 0x6eb3f879cb30fe243b4dfee438691c043318585733ff
// PUSH1 0
// MSTORE ;; at this point, memory locations mem[10] through
// ;; mem[31] contain the runtime portion of the child
// ;; contract. all that's left to do is to RETURN this
// ;; chunk of memory.
// PUSH1 22 ;; length
// PUSH1 10 ;; offset
// RETURN
// Or in binary: 756eb3f879cb30fe243b4dfee438691c043318585733ff6000526016600af3
// Almost done! All we have to do is put this short (31 bytes) blob into
// memory and call CREATE with the appropriate offsets.
let solidity_free_mem_ptr := mload(0x40)
mstore(solidity_free_mem_ptr, 0x00756eb3f879cb30fe243b4dfee438691c043318585733ff6000526016600af3)
addr := create(0, add(solidity_free_mem_ptr, 1), 31)
}
仔细研究这个汇编代码可以更好地理解 EVM。我个人的观点是,合约开发者在原则上不应该使用汇编,但也有例外,那就是在设计上要求最小化并要求极高效率的合约。这个合约,还有 EIP-1167,就是例子。
PUSH15
开始:地址本来有 20 个字节,但在这里,我们想把 15 个字节推入这个栈(这是最优实现),因为我们使用了 vanity-address 风格的技巧,它会重复地哈希,直到找到符合需要的地址,所以前面 5 个字节都是 0。剩下还需要 5 个 0,作为默认的一部分填充进去,组成 32 个字节,也就是 EVM 里面 word 的大小。这里的优化是很重要的,因为用来创建 chile 合约所用的 gas 可以认为是整个 GasToken 方案的开销。CALLER
把合约调用者的地址推入栈中。 XOR
会从栈中弹出两个物,然后把这两个值的按位异或运算结果推入栈中。如果这两个值相等,则栈顶为 0,反之则是一个非零的数字。 PC
在与此操作对应的增量出现之前从程序计数器处获得一个值,并推入栈中。 JUMPI
,一个条件跳转,从栈中取出栈顶的两个值,一个条件和一个目标,如果条件为真,就跳到目标,如果条件不为真,那就失败。JUMPI
的结果不是 JUMPDEST
操作码,EVM 就会回滚,这保证了调用者是 parent 合约(满足 !=
条件)。失败的路径结束后,就把 parent 合约的地址推入栈中,当下一次 slefdestruct
操作执行时,弹出栈顶的 word,作为 gas 退款的目标。(未完)
(文内有许多超链接,可点击左下 ”阅读原文“ 从 EthFans 网站上获取)
原文链接:
https://medium.com/coinmonks/gastoken-or-how-i-learned-to-stop-worrying-and-love-gas-price-surges-6aaee9fb0ba3
作者: Aodhgan Gleeson
你可能还喜欢: