EIP-5283: 用于可重入保护的信号量
一种基于预编译、使用调用堆栈的可并行化的可重入保护
Authors | Sergio D. Lerner (@SergioDemianLerner) |
---|---|
Created | 2022-07-17 |
Discussion Link | https://ethereum-magicians.org/t/eip-5283-a-semaphore-for-parallelizable-reentrancy-protection/10236 |
Requires | EIP-20, EIP-1283, EIP-1352 |
摘要
此 EIP 提议添加一个预编译合约,该合约提供一个信号量函数,用于创建一种新型的可重入保护守卫 (RPG)。此函数旨在替换典型的基于修改合约存储单元的 RPG。其优点是,基于预编译的 RPG 不写入存储,因此它使合约能够向前兼容所有为 EVM 事务的多线程执行提供细粒度(即单元级别)并行化的设计。
动机
典型的智能合约 RPG 使用合约存储单元。该算法很简单:代码检查存储单元在进入时是否为 0(或任何其他预定义的常量),如果不是则中止,然后将其设置为 1。在执行所需的代码后,它会在退出前将单元重置回 0。这是 OpenZeppelin 的 ReentrancyGuard 中实现的算法。该算法导致 RPG 的存储单元上出现读写模式。对于所有试图提供细粒度并行化(在存储单元级别检测冲突)的已知设计,此模式会阻止智能合约执行的并行化。
几个基于 EVM 的区块链已经成功测试了 EVM 并行化的设计。最好的结果是通过细粒度的并行化获得的,其中通过跟踪单个存储单元的写入和读取来检测冲突。基于跟踪帐户或合约使用的设计仅提供较小的优势,因为大多数事务使用相同的 EIP-20 合约。
总而言之,今天唯一可用的 RPG 构造是基于使用合约存储单元。这种构造很干净,但与事务执行并行化不向前兼容。
规范
从激活区块(待定)开始,在地址 0x0A
创建一个新的预编译合约 Semaphore
。当调用 Semaphore
时,如果调用者地址在调用堆栈中出现多次,则合约的行为就像第一个指令是 REVERT
,因此 CALL 返回 0。否则,它不执行任何代码并返回 1。合约执行的 Gas 成本设置为 100,该成本独立于调用结果消耗。
理由
地址 0x0A
是 EIP-1352 定义的范围内下一个可用的地址。
示例用法
pragma solidity ^0.8.0;
abstract contract ReentrancyGuard2 {
uint8 constant SemaphoreAddress = 0x0A;
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* 防止合约直接或间接调用自身。
* Calling a `nonReentrant` function from another `nonReentrant`
* 支持从另一个 `nonReentrant` 函数调用 `nonReentrant` 函数。
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
}
function _nonReentrantBefore() private {
assembly {
if iszero(staticcall(1000,SemaphoreAddress, 0, 0, 0, 0)) {
revert(0, 0)
}
}
}
}
可并行化的基于存储的 RPG
并行化使用存储 RPG 构造的先前合约的唯一方法是 VM 自动检测到存储变量用于 RPG,并证明它可以按要求工作。这需要静态代码分析。由于以下两个原因,这很难在共识中实现。首先,检测和/或证明的 CPU 成本可能很高。其次,某些合约函数可能不受 RPG 的保护,这意味着某些执行路径不会更改 RPG,这可能会使证明复杂化。因此,本提案旨在保护未来的合约并使其可并行化,而不是并行化已部署的合约。
替代方案
在 EVM 上实现 RPG 有替代设计:
- 瞬态存储操作码 (
TLOAD
/TSTORE
) 提供合约状态,该状态在同一事务中的调用之间保持,但之后不会提交到世界状态。这些操作码还支持细粒度的并行化。 - 一个操作码
SSTORE_COUNT
,它检索执行的SSTORE
指令的数量。它还支持细粒度的执行并行化,但SSTORE_COUNT
更难以正确使用,因为它返回执行的SSTORE
操作码的数量,而不是可重入调用的数量。必须从此值中扣除可重入性。 - 一个新的
LOCKCALL
操作码,其工作方式类似于STATICALL
,但仅阻止调用者合约中的存储写入。这会导致更便宜的 RPG,但它不允许某些合约函数免受 RPG 的影响。
所有这些替代提案都有一个缺点,即它们创建了新的操作码,如果可以使用预编译以相同的 Gas 成本实现相同的功能,则不鼓励这样做。新的操作码需要修改编译器、调试器和静态分析工具。
Gas 成本
100 的 Gas 成本表示最坏情况下的资源消耗,当堆栈几乎已满(大约 400 个地址)并被完全扫描时,会发生这种情况。由于堆栈始终存在于 RAM 中,因此扫描速度很快。
注意:一旦代码在 geth 中实现,就可以对其进行基准测试,并且可以重新评估成本,因为在实践中可能会更低。由于预编译调用当前花费 700 Gas,因此堆栈扫描的成本对预编译调用的总成本影响很小(总共 800 Gas)。
基于存储的 RPG 当前花费 200 Gas(因为 EIP-1283 中引入的节省)。使用 Semaphore
预编译作为可重入性检查当前需要花费 800 Gas(来自函数修饰符之一的单个调用)。虽然此成本高于传统 RPG 成本,因此不鼓励使用,但它仍然远低于 pre-EIP-1283 成本。如果实现了预编译调用成本的降低,则使用 Semaphore
预编译的成本将降低到大约 140 Gas,低于当前基于存储的 RPG 消耗的 200 Gas。为了鼓励使用基于预编译的 RPG,建议将此 EIP 与降低预编译调用成本一起实施。
向后兼容性
此更改需要硬分叉,因此必须更新所有完整节点。
测试用例
contract Test is ReentrancyGuard2 {
function second() external nonReentrant {
}
function first() external nonReentrant {
this.second();
}
}
直接从事务调用 second()
不会恢复,但调用 first()
会恢复。
安全考虑
需要讨论。
版权
在 CC0 下放弃版权及相关权利。
Citation
Please cite this document as:
Sergio D. Lerner (@SergioDemianLerner), "EIP-5283: 用于可重入保护的信号量 [DRAFT]," Ethereum Improvement Proposals, no. 5283, July 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5283.