简介
EIP-150,即 Ethereum 改进提案 150,是 Ethereum 区块链的一次协议升级。它于 2016 年 3 月 18 日提出,并于 2016 年 7 月 20 日作为 Ethereum Byzantium 硬分叉的一部分实施。协议中进行了多项更改,但我们将重点关注其引入的 63/64 规则。
在深入探讨 EIP-150 的具体细节之前,了解 Ethereum 中 gas 和合约调用的概念非常重要。
作者
本文由 tanim0la(Twitter)联合撰写,是 RareSkills Technical Writing Program 的一部分。
Ethereum 中的 gas 概念
Gas 是衡量在 Ethereum Virtual Machine (EVM) 上执行特定操作或合约所需计算能力的单位。
EVM 上的每个操作或合约都需要一定数量的 gas 才能执行,这必须由用户以 Ether 的形式支付。每当用户发起交易时,他们都有义务支付其交易执行的所有操作的累计费用。
对于基本的 Ethereum 转账,此类操作的成本恰好为 21,000 gas;然而,更复杂的操作可能需要多得多的 gas 单位,可能高达数百万。
合约调用
在 Ethereum 中,合约(称为“调用者”)能够通过特殊的 opcode(如 CALL、STATICCALL 和 DELEGATECALL)调用其他合约(称为“被调用者”)。当发生这种情况时,“被调用者”会收到特定数量的 gas,这与它们通过交易被直接调用时收到的 gas 类似。
分配的 gas 数量部分由调用者决定,正如在 opcode 参数中所指定的那样。如需了解更多信息,请参考此处提供的 CALL 规范示例。如果被调用者收到不足的 gas,其操作将被 revert,触发“out of gas”异常。
EIP-150 规范
在此之前,调用者可以将提供给他们的所有 gas 发送给被调用者。然而,由于每次调用的 gas 成本很低,这导致可能会出现无穷无尽的合约调用其他合约的情况。
为了防止在 Ethereum 节点的实现中出现“stack too deep”问题,最大深度被限制为 1024,并且该限制保留至今。当达到此深度时,最后一次调用将发生 revert。
因此,交易签名者能够通过让交易经过一系列调用,直到达到 1023 的深度,然后再调用目标智能合约,从而保证特定的调用必定发生 revert。这种类型的攻击被称为“Call Depth Attack”。
以下是一个容易受到此类攻击的合约示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.5;
// DO NOT USE!!!
contract Auction {
address highestBidder;
uint256 highestBid;
function bid() external payable {
if (msg.value < highestBid) revert();
if (highestBidder != address(0)) {
payable(highestBidder).send(highestBid); // refund previous bidder
}
highestBidder = msg.sender;
highestBid = msg.value;
}
}
在实施 EIP-150 之前,上述合约可能容易受到“Call Depth Attack”。这是因为恶意竞标者可以启动对自身的递归调用,导致堆栈深度增加到 1023,然后再调用 bid() 函数。结果,send(highestBid) 调用将静默失败,这意味着前一个竞标者将无法收到退款,而新的竞标者仍然会成为最高竞标者。
EIP-150 提出的解决方案是,在调用了被调用者的合约后,在调用者合约内保留一部分可用 gas(即调用消耗的 gas 不能超过父级的 63/64)。
为父级保留的 gas 公式:
Reserved portion of the available gas = available gas at Stack depth N - ((63/64) * available gas at Stack depth N)
让我们测试一下上面的公式!
Assume: available gas at Stack depth 0 = 1000
Reserved portion of the available gas = 1000 - ((63/64) * 1000) = 15
63/64 规则:被调用者可以接收的 Gas
下面的公式计算了在任意堆栈深度下的可用 gas。此外,可用 gas 与堆栈深度成反比(堆栈深度越深,可用 gas 越少)。
Gas available at Stack depth 0 = Initial gas available * (63/64)^0
Gas available at Stack depth 1 = Initial gas available * (63/64)^1
Gas available at Stack depth 2 = Initial gas available * (63/64)^2
Gas available at Stack depth 3 = Initial gas available * (63/64)^3
.
.
.
Gas available at Stack depth N = Initial gas available * (63/64)^N
以下是一些示例值。
Assume; Initial gas available = 3000
Gas available at Stack depth 10 = 3000 * (63/64)^10 = 2562
Gas available at Stack depth 20 = 3000 * (63/64)^20 = 2189

由于每增加一个深度层级 gas 就会迅速减少,递归深度会自然而然地受到限制。尽管在当前的 Ethereum 实现中仍然存在 1024 的堆栈深度限制,但实际上已经无法达到了。
除了前面提到的修改之外,EIP-150 还引入了对 CALL* opcodes 的更改,其中提供的 gas 现在是一个最大值而不是严格值。这意味着如果被调用的合约(被调用者)中的可用 gas 低于提供给 opcode 的指定值,调用仍然会继续进行,只是气量会减少而不是直接失败,这在以前的版本中并非如此。
结论
总而言之,引入 EIP-150 是为了防止 Call Depth Attack。它通过强制执行 63/64 规则规范来实现这一点,这意味着即使在调用中明确转发所有的 gas left,仍会有一部分 gas 为调用者合约保留。
了解更多
加入我们的 Solidity Bootcamp,全面深入了解 EVM 和 Ethereum 协议。
最初发布于 2023 年 3 月 23 日