Alert Source Discuss
🚧 Stagnant Standards Track: Core

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,该成本独立于调用结果消耗。

理由

地址 0x0AEIP-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 有替代设计:

  1. 瞬态存储操作码 (TLOAD/TSTORE) 提供合约状态,该状态在同一事务中的调用之间保持,但之后不会提交到世界状态。这些操作码还支持细粒度的并行化。
  2. 一个操作码 SSTORE_COUNT,它检索执行的 SSTORE 指令的数量。它还支持细粒度的执行并行化,但 SSTORE_COUNT 更难以正确使用,因为它返回执行的 SSTORE 操作码的数量,而不是可重入调用的数量。必须从此值中扣除可重入性。
  3. 一个新的 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.