GasToken:我为何不再担心 gas 价格飙升(上)-区块链毕设资料

这是qklbishe.com第4456 篇区块链毕业设计资料
提供毕设资料分析,通过本文《GasToken:我为何不再担心 gas 价格飙升(上)-区块链毕设资料》可以理解其中的代码原理,这是一篇很好的区块链代码论文资料

引介

GasToken:我为何不再担心 gas 价格飙升(上)

Ajian   |     |   52 次阅读

引介 安全性 以太坊

本文旨在探索 EVM 的 gas 机制,尤其是 GasToken 的 EVM gas 机制。首先,为了降低技术理解的难度,本文需要先给出一段介绍。如果你不想了解底层机制,可以直接跳到后文的 “具体实现细节” 一节开始阅读。

GasToken:我为何不再担心 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 使用量。gas 使用量与 gasPrice(以太坊交易中的用户设置字段,即该用户愿意为每单位的 gas 支付的 eth 价格)的乘积会转换成 Wei,也就是以太坊原生代币 ETH。更多关于交易的基础知识,可以参见这篇文章。

区块的 gas 上限

一般情况下,矿工都是依据最高价拍卖模型将交易打包到区块内的。备受期待的 EIP 1559 意图将这一动态转变为更有效的结构,另外还有交易费必须使用以太币支付所带来的副作用。然而,这篇文章不是专门介绍 EIP 1559 的,EIP 1559 这个主题本身就具有非常深远的影响。在这篇文章中,我们将聚焦于促成 GasToken 的 gas 机制/经济学。

每个区块都有一个相关的 gas 上限,目前(2020 年 11 月)是 1250 万 gas。因此,由于区块容量有限,形成了一个竞争激烈的 “区块空间” 市场。虽然验证时间占区块传播时间的比例低于 1%,但这个上限的存在还是保证了网络的安全性。将区块 gas 上限定得太高,节点很难赶在下一个区块挖出之前执行完区块中的所有交易(也有可能跳过这些交易 —— 具体参见验证者困境)。将区块 gas 上限定得太低,就会导致网络拥堵和缺乏实用性。关于这里的权衡关系,请参见这篇文章。

有趣的是,矿工可以使用节点 cli flag 来标记他们所期望的区块 gas 上限,但是修改 gas 上限(例如最近从 8M 上调至 12.5 M)似乎主要发生在 “社会层面(推特)” 上。正是因为矿工可以上调/下调每个区块 gas 上限的机制,让我们明白了下图为什么会出现峰值:

GasToken:我为何不再担心 gas 价格飙升(上)

– 上图显示了区块 gas 上限随时间流逝的变化情况。请注意,区块 gas 上限之所以会在 2016 年底大幅降低,是因为当时遭到了 DDoS 攻击。(来源:https://blog.ethereum.org/2016/09/22/ethereum-network-currently-undergoing-dos-attack/) –

区块空间拍卖被认为在经济学/机制设计方面开辟了新的领域,因为传统拍卖理论是以免费投标的假设为前提的。以太坊交易并非如此,交易费率必须在一定的阈值之上,而且一旦交易被广播到点对点网络上,就不受控制了。

接下来进入正题

可以说,最有趣的操作码同时也是成本较高的操作码,如 SSTORECREATECREATE2SELFDESTRUCT。这些操作码的共同点是,它们都涉及状态,因此也涉及硬盘读写(以太坊网络的节点通常使用固态硬盘)。这些操作码成本更高,因为它们会影响永久存储和全局状态树。

什么是 GasToken

GasToken 巧妙地利用了 gas 定价系统。它利用的是清理状态、清理存储插槽(storage slot)和删除带有自毁操作码的合约(这些操作都可以删减全局状态树)所收到的 gas 退款。这些操作都可以被认为具备负 gas 价格。

  • 清理/自毁合约:- 24,000 gas
  • 清理/删除存储:-15,000 gas

当 EVM 执行这类操作时,gas 退款是通过一个独立的交易退款计量器来计算的。gas 退款只会在交易结束时提供。另外,最高 gas 退款量是该交易所消耗 gas 量的一半。

理想情况是在网络 gas 价格较低时写入状态,并在 gas 价格较高时删除状态。由于以 Wei/ETH 为计价单位的总费用是 gas 使用量和 gas 价格的乘积,当 gas 价格较高时,减少 gas 使用量会导致总费用降低。

GasToken 的正统实现很好地体现了名称中的 “token(代币)”部分,因为它与 ERC-20 代币相似,并带有 approvetransferFrom操作码,可以称为多步骤交易的一部分。最初,GasToken 有两种变体,分别采用不同的设计:GST1 和 GST2。GST1 使用的是存储成本和退款机制,GST2 使用的是 CREATE 和自毁机制。这些变体采取不同的节约方案,具体取决于 gas 价格差值比(铸造代币和释放代币时的 gas 价格差值比)。由于 gas 价格率更高,GST2 更能节约 gas。

开采或 “铸造” GasToken 就是将其 写入存储/创建合约,而销毁或 “释放” GasToken 就是减少用户持有的 GasToken 数量并删除状态存储插槽。虽然正统的 GasToken 很流行,但是许多开发者选择克隆这一功能,并放到他们自己的系统合约中使用,从而减少成本和设计复杂性。

具体实现细节

GST1 —— 基于存储

从智能合约的层面来看,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,就是例子。

优化

第 4 行和第 5 行中展示的时 child 合约中的回调函数(fallback function)的伪代码 —— 为什么要用回调函数?因为我们希望 child 合约能尽可能简单,简单到只有一个函数。

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 退款的目标。

(未完)


文章部分来自互联网,侵权联系删除
www.qklbishe.com

区块链毕设网(www.qklbishe.com)全网最靠谱的原创区块链毕设代做网站
部分资料来自网络,侵权联系删除!
资源收费仅为搬运整理打赏费用,用户自愿支付 !
qklbishe.com区块链毕设代做网专注|以太坊fabric-计算机|java|毕业设计|代做平台 » GasToken:我为何不再担心 gas 价格飙升(上)-区块链毕设资料

提供最优质的资源集合

立即查看 了解详情