真实世界以太坊合约的成本
真实世界以太坊合约的成本
原文:https://medium.com/hackernoon/costs-of-a-real-world-ethereum-contract-2033511b3214
*天然气价格 PSA*(2017–08–23):撰写本文时的天然气价格中值在 20 Gwei 的范围内,并将继续如此。这远远大于在eth asstation上发现的典型平均值和安全低(分别为 4 和 0.5 Gwei)。中位数如此之高,是因为许多钱包里都有糟糕的汽油价格违约。我 *强烈建议* 使用加油站的平均气价或更低的气价,以避免支付高额费用,并帮助降低气价的市场价格。
我之前讨论过计算以太坊智能合约的成本,方法是查看称为操作码的低级操作,以及运行这些操作码的市场价格(天然气价格)。给出的例子很简单,但有点做作,所以我决定采用上周的分析,并将它从头到尾应用到一个实际的智能合同中。
我正在开发一系列免费开放使用的简单智能合同。我们将使用这个系列的第一个— 代管——并深入探讨与之相关的成本。

合同
合同的快速背景分析。该合同涉及三方——发送方、接收方和商定的仲裁方。发送方用一些以太网初始化合同,并指定接收方、仲裁人和截止日期。如果双方中的任何一方(发送方、接收方或仲裁方)通过[confirm](https://github.com/djrtwo/simple-contracts/blob/0a30f43929dcf6964b833348b62544977078cddb/contracts/escrow.sol#L35)功能确认付款,资金将发放给接收方。如果在到期之前没有进行两次确认,那么发送者可以使托管协议无效,并通过[void](https://github.com/djrtwo/simple-contracts/blob/0a30f43929dcf6964b833348b62544977078cddb/contracts/escrow.sol#L47)函数调用取回他们的资金。
成本
我在 Rinkeby testnet 上部署了这个契约的一个实例,可以在 Etherscan 上看到。该合同有三个相关的交易,都使用 20 Gwei 的气价,这是在 Eth 加油站看到的中间价格。
- 第一笔交易初始化合约,并在合约中存入 0.5 乙醚。交易成本:0 。 01072934 乙醚(300 美元/乙醚,3.21 美元)
- 在第二笔交易中,发送方呼叫
confirm。交易成本:0 。 00093492 乙醚(300 美元/乙醚,0.28 美元) - 在第三笔交易中,仲裁员调用
confirm,资金被分配给接收方。交易成本:0 。 00164754 乙醚(300 美元/乙醚,0.49 美元)
如果我愿意等待大约 20 分钟来处理我的交易,我可以以 0.5 Gwei 的价格在主网上获得这些交易(根据 Eth 加油站的“安全低”)。在这种天然气价格下,部署合同的成本约为 0.07 美元,其他两项交易的成本为 0.01 美元或更少,价格为 300 美元/ETH。
考虑到这是一个固定成本,在这个托管合同中持有任何数量的乙醚,这些费用似乎是合理的。您可能需要向仲裁人支付一定金额才能参与,但仲裁人的大量成本和风险(安全地持有和分散资金)已经消除。你将不得不单独为仲裁——决定——而不是所有的簿记付费。
如果你只是想要一个真实世界例子的直接成本,这是一个很好的地方。文章的其余部分深入探讨了这些成本的确切来源。
继续读下去,后果自负!
部署成本
在目前用于智能合约的主要编程语言 solidity 中,合约通过构造函数初始化。您可以在托管构造函数中看到,我们传入了 4 条数据——所涉及的三方的地址和一个未来合同可能失效的时间戳。
该契约的初始化事务是迄今为止开销最大的操作。它需要 536467 gas 来部署契约和执行构造器代码。在每种气体 20 Gwei 的情况下,部署合同成本为 0 。 01072934 乙醚,按目前 300 美元/乙醚的汇率,约合 3.21 美元。
构造函数
为了进一步检查初始化,让我们看一下事务的 VM 跟踪。这显示了 EVM 在托管合同的构造函数中执行的操作码。这占了所用气体的 113539。开销最大的操作是许多存储,用于存储参与者的地址、过期时间戳,以及初始化参与者数组和确认映射的一些内存位置。仅这些储气库操作就消耗了 110000 份天然气,约占建造商所用天然气的 97%。其余的操作码用于检查时间戳的有效性和获取存储的所有内容的业务逻辑。
其余的呢?
施工方仅占本次交易中使用的所有天然气的 20%左右。剩下的 80%都花在哪里了?
在该交易中,我们将允许使用的最大气体(“气体限制”)指定为 1000000 气体。EVM 从这个数字开始,并随着每次操作倒计时,以确保有足够的剩余气体。如果 EVM 在代码执行过程中遇到零汽油,则交易失败,更改被撤消,与汽油相关的费用仍然支付给矿工。
我们可以在 VM 跟踪中看到,我们在剩余 837872 gas 处开始执行构造函数。这意味着当我们到达构造器时,162128 ( 1000000-837872)或者总气体的大约 30%已经被使用。
在我们开始执行代码之前,什么东西花费了这么多?第一,有 21000 气基线交易费。这笔费用在黄皮书中被称为 G 交易,在以太坊网络的所有交易中支付。接下来是额外的 32000 gas (G create 支付,因为这是一个合同创建交易。我们有 53000 个气体——在构造器之前还有 109128 个气体没有计算。这些气体的大部分用于支付在以太网扫描的“输入数据”字段中看到的交易数据的大小。这是 3556 个十六进制“半字节”或 1778 个字节的数据。如黄皮书第 20 页所示,G txdatazero 成本为 4 gas/字节,Gtxdata zero成本为 68 gas/字节。我们可以用下面的等式 — x * 68 + (1778-x) * 4 = 109128来计算非零字节和零字节的个数,其中x是非零字节的个数,1778-x是零字节的个数。求解x得到x = 1594,所以有 1594 个非零字节和 184 个零字节。作为检查,我写了一个 python 脚本来计算 txdata 中的零和非零字节。这些数字加起来。
剩下的 50%
这仍然留下大约 50%的气体在构造者之后被使用。我们的汽油限额从 1000000 开始。构造器代码的最后一条指令留给我们 724333 可用气体。该交易总共使用了 536467 天然气,因此在整个交易结束时可用天然气为(1000000–536467 ) 463533 天然气。构造器代码结束时的可用数量减去交易结束时的可用数量等于构造器代码结束后的用气量— ( 72433–463533 ) 260800 气。
此时,我们已经支付了交易数据和合同初始化的费用,但是对合同的未来调用怎么办?未来的调用将必须执行代码,因此代码必须在契约本身的状态中处于链上状态。黄皮书的第 9 页讨论了为向区块链状态添加字节码而支付的“代码押金”的成本。cost = G*codedeposit* * |o|其中o是“运行时字节码”,而G*codedeposit*是 200 gas/字节。运行时字节码是在事务中发送的原始字节码,但是去掉了构造函数和通用初始化代码。因为这个初始化代码只在初始化时执行,所以把它提取出来是为了省钱和节省节点操作符的存储空间。
Solidity 编译器输出字节码和运行时字节码。浏览器内 solidity 编辑器/编译器 Remix 也在“合同详情”下给你展示了这两个值。混音对于一些快速信息和健全检查来说是很棒的。
从编译器输出中,我看到字节码的大小是 1650 字节,而运行时字节码的大小是 1304 字节。这意味着 346 字节(1650–1304)是初始化代码。因此,存放在运行时字节码上的代码成本是1304 * 200 = 260800 gas,这正是我们预期的金额。
同样,契约初始化事务是迄今为止与托管契约相关的最昂贵的操作,并且可能是大多数契约的操作。我们发送一个充满字节码的事务,运行初始化内存中多个位置的构造器代码,然后为我们永久留在区块链上的所有代码付费。增加区块链(全球分布和复制的数据库)的大小是并且应该是昂贵的。
要确认的成本
确认#1
托管合同上的第一次确认只是更新状态以显示用户已确认。这笔钱仍然持有,直到第二次确认才释放。
该交易的气体限制为 90000 气体,使用了 46746 气体。代码执行从 68728 gas 开始,到 43254 gas 结束,所以运行代码使用68728 — 43254 = 25474 gas。如果你看一下的痕迹,有一家店的汽油价格是 20000 英镑。这是为了存储用户发送确认的事实。这是代码执行成本的主要部分。下一个最昂贵的操作是大量的 SLOADs,从内存中加载单词,每个花费 200 gas。
因为代码执行从最初的 90000 气体限制的 68728 开始,所以在此之前使用了90000 – 68728 = 21272 gas。21000 汽油是任何交易的基本成本,因此还有剩余的 272 汽油需要考虑。请记住,非零事务数据成本为 68 gas/字节。这个事务有 4 个字节的数据开销4 * 68 = 272 gas。
21272 预执行气体加上 25474 执行气体等于 46746 气体,这是总数——所有气体都考虑在内。
确认#2
托管合同上的第二次确认既更新状态以显示用户已确认,又将资金发放给接收方。我们预计这次交易会比第一次确认要贵一点,因为它做的是第一次确认所做的事情,再加上发送以太网。
该交易的气体限制为 90000 气体,使用了 82377 气体。代码执行从 68728 gas 开始,到 7623 gas 结束,所以运行代码使用68728 – 7623 = 61105 gas。代码执行开始于与第一次确认完全相同的 gas 计数,因此在代码执行之前使用了 21272 gas,考虑了 21000 基本事务成本加上 4 字节的 txdata。代码执行中使用的数量,61105 气体,加上代码执行前使用的数量,21272 气体,等于 82377 气体——所有气体都计算在内。
让我们更深入地看看虚拟机痕迹。像前面的事务一样,有一个存储该用户确认的事实的存储库。剩余的 41005 gas 大部分用于名为 CALL 的操作码。这次行动使用了 32400 气体。
在跟踪中,看起来 39981 gas 被用于一个调用操作,但是这有点误导。这实际上只是分配给呼叫操作的气体量,而不是总消耗量。Gas 被分配给调用操作,而不是简单地花费,因为如果接收地址是一个约定,调用可以执行代码,而运行代码所需的确切 gas 量在执行之前是不知道的。我们可以查看调用前后的操作来计算实际使用了多少。调用前的操作在 40064 gas 结束,调用后的操作在 7664 gas 开始。所以调用操作夹在 uses(40064–7664)40064 – 7664 = 32400 gas之间。
让我们看一下黄皮书的第 20 页和第 29 页,以了解呼叫操作的成本。执行通话无论如何都要花 700 气(G*call* ) 。使用 CALL 传输一个非零量的乙醚需要额外花费 9000 气(G*callvalue*)。一个给区块链州增加新账户的呼叫操作需要额外花费 25000 燃气(G*newaccount*)。在这种情况下,调用确实向区块链添加了一个新帐户,因为接收方地址以前没有与之相关联的值或合同。具有非零价值转移的调用操作也获得 2300 汽油(G*callstipend*)的津贴,该津贴从与操作相关的其他成本中减去。所以总的来说
700 (G*call*) + 9000 (Gcallvalue) + 25000 (G*newaccount*) - 2300 (G*callstipend*) = 32400 gas
呼叫气体被计算在内。
用于G*newaccount*的昂贵的 25000 燃气有点让人吃惊。如果收款人以前是一个非空账户,我们的汽油费用会少得多。在进行天然气计算/估算时,根据当前状态,会有许多类似的复杂情况。
作废成本
我将把计算和分析执行一个成功的[void](https://github.com/djrtwo/simple-contracts/blob/0a30f43929dcf6964b833348b62544977078cddb/contracts/escrow.sol#L47)交易的成本留给读者。我们预计该操作的开销与第二次确认的开销类似,因为它通过调用操作进行了价值转移,但是它的开销应该会稍微小一些,因为它没有通过昂贵的存储更新确认的状态。
进一步说明
应该注意的是,本应该完全避免confirm中昂贵的调用操作。一个效果后直接发资金,其实是实打实的反模式。模式是通过一个函数调用来更新状态,这个函数调用允许随后的撤销。在托管契约中,第二次确认应该将契约的状态更改为“已确认”,禁用对void的调用,并使接收者能够调用名为withdraw的新函数。这将避免与呼叫相关的意外费用或操作相关的问题。
该分析中的危险信号是,由于不知道将调用和执行什么样的帐户/代码,我们的运营可能会大幅增加汽油成本。这种未知会导致诸如 DAO 重入错误之类的错误。
智能合约非常强大,但是,正如我们所看到的,可能非常复杂。围绕它们的语言、工具和最佳实践仍处于起步阶段。我敦促社区将大部分精力集中在构建底层工具和架构上,以确保我们能够创建正确且负担得起的智能合同。
如果我们要做这件事,那就好好做。