Alert Source Discuss
⚠️ Draft Standards Track: Core

EIP-7851: 停用/重新激活委托 EOA 的密钥

引入一个新的预编译合约,供具有委托代码的 EOA 停用或重新激活私钥。

Authors Liyi Guo (@colinlyguo)
Created 2024-12-27
Discussion Link https://ethereum-magicians.org/t/eip-7851-deactivate-reactivate-a-delegated-eoas-key/22344
Requires EIP-7702

摘要

本 EIP 引入了一个预编译合约,该合约允许通过 EIP-7702 将控制权委托给智能合约的外部拥有账户 (EOA) 停用或重新激活其私钥。此设计不需要额外的存储字段或账户状态更改。通过利用委托代码,可以通过诸如社交恢复等机制安全地执行重新激活。

动机

EIP-7702 使 EOA 能够获得智能合约功能,但 EOA 的私钥仍然保留对账户的完全控制权。

通过此 EIP,EOA 可以完全迁移到智能合约钱包,同时保留通过重新激活进行私钥恢复的选项。灵活的停用和重新激活设计也为原生账户抽象铺平了道路。例如,EIP-7701

规范

本文档中的关键词“必须”,“不得”,“必需”,“应该”,“不应该”,“推荐”,“不推荐”,“可以”和“可选”应按照 RFC 2119 和 RFC 8174 中的描述进行解释。

参数

常量
PRECOMPILE_ADDRESS 0xTBD
PRECOMPILE_GAS_COST 13000

定义

  • || 是字节/字节数组连接运算符。

委托代码编码

停用状态通过在委托代码的末尾附加或删除 0x00 字节来编码。两个状态之间的转换如下:

  • 激活状态: 0xef0100 || address。私钥处于活动状态,可以签名交易。
  • 停用状态: 0xef0100 || address || 0x00。私钥已停用,无法签名交易或 EIP-7702 委托授权。

预编译合约

一个新的预编译合约在地址 PRECOMPILE_ADDRESS 处引入。它花费 PRECOMPILE_GAS_COST 并执行以下逻辑:

  • 如果出现以下情况,则返回预编译合约错误,消耗所有 gas,并且不进行状态更改:
    • Gas 不足。
    • 通过 STATICCALL 调用(即,在只读上下文中)。
    • 调用者不是具有委托代码的 EOA(根据 EIP-7702,前缀为 0xef0100)。
  • 根据长度更新调用者的委托代码:
    • 23 字节 (0xef0100 || address): 附加 0x00 以停用私钥授权。
    • 24 字节 (0xef0100 || address || 0x00): 删除最后一个字节 0x00 以激活私钥授权。
  • 将更新后的代码保存为调用者的新账户代码。

交易验证

必须在以下情况下执行验证以检查私钥是否已停用(由代码前缀 0xef0100 和代码长度 24 标识):

  • 在执行之前的交易有效性检查期间,节点必须拒绝使用停用的私钥签名的交易,以确保此类交易不会包含在区块中。
  • 接收新交易时,必须将相同的验证应用于交易池,以防止无效交易在网络中传播。
  • 由具有停用私钥的授权机构发布的任何 EIP-7702 授权必须被视为无效并跳过。

原理

预编译合约的成本

PRECOMPILE_GAS_COST 表示验证和潜在更新账户代码所需的 gas。调用此预编译合约的合理成本可以通过分析其对节点的影响来确定:

  • 读取地址的代码: 2600
  • 更改代码哈希值(从非零到非零): 5000
  • 部署最多 24 字节的代码: 200 * 24 = 4800

主要操作总共消耗 12400 gas。为了考虑额外的开销,例如状态转换期间的上下文切换,以及代码前缀和长度验证,成本四舍五入为 13000

额外的交易验证开销

由于 EIP-3607EIP-7702,节点已经在交易验证期间加载账户代码,以验证发送者是否为 EOA(空代码或具有前缀 0xef0100 的代码)。此 EIP 仅在加载代码后引入代码长度检查,从而导致额外的开销最小。类似地,EIP-7702 的授权验证已经涉及检索账户代码,此 EIP 仅添加代码长度检查,与代码读取相比,这可以忽略不计。

交易池已经执行基于状态的检查,例如 nonce 和余额的检查。此 EIP 添加了账户代码读取和长度检查,它们加在一起与 nonce 和余额验证相当。交易池中预期的 DoS 防御机制也支持这些额外的检查。

另一种 EOA 弃用方法

另一种弃用方法涉及使用硬分叉来编辑所有现有和新的 EOA 到预先编写的可升级智能合约,这些合约利用原始 EOA 私钥进行授权。用户可以添加和替换密钥,或将智能合约升级到其他实现。但是,此方法与已经委托给智能合约的 EOA 不兼容,因为它会覆盖现有的智能合约实现。此 EIP 旨在填补此迁移空白。

使用预编译合约

停用和重新激活 EOA 私钥的替代方法包括:

  • 添加新的交易类型:引入新的交易类型可以提供一种停用和重新激活 EOA 私钥的机制。但是,重新激活私钥将依赖于委托合约作为授权者,这使得定义新交易类型的规则变得复杂。
  • 部署智能合约:与重用代码字段相比,使用智能合约的成本更高:(i)它需要新的合约存储槽来跟踪每个地址的停用状态(ii)与预编译合约相比,执行字节码增加了节点的开销,(iii)在交易验证期间,重用代码字段允许将停用状态检查与某些场景中现有的代码加载相结合,从而减少了对额外存储读取的需求,如 额外的交易验证开销 部分中所述。

协议内重新激活

此方法确保与未来迁移的最大兼容性。EOA 可以重新激活其私钥,将其账户委托给 EIP-7701 合约,然后再次停用其私钥。这避免了合约升级的限制。例如,为了在升级到 EOF 合约时删除旧的代理合约(减少 gas 开销),可以重新激活 EOA 并将其重新委托给 EOF 代理合约。

重新激活只能由委托合约执行。由于重新激活私钥授予对钱包的完全控制权,因此钱包必须通过严格的安全措施来实现此接口。这些措施应将重新激活视为最高级别的授权,相当于完全的钱包所有权。用户应仅将其 EOA 委托给经过彻底审计并遵循安全最佳实践的钱包。

避免委托代码前缀修改

此 EIP 将字节 (0x00) 附加到委托代码,而不是修改 EIP-7702 的前缀 (0xef0100),以确保向前兼容。如果将来引入了诸如 0xef0101 之类的新前缀,则更改前缀以表示停用状态(例如,0xef01ff)使得不清楚在重新激活时要恢复哪个前缀(0xef01000xef0101)。

避免账户状态更改

另一种方法是在账户状态中添加一个 deactivated 字段来跟踪状态。但是,当启用此 EIP 时,此方法将引入与此可选字段相关的向后兼容性逻辑和更多测试向量,因为现有账户中不存在该字段。

向后兼容性

当私钥被停用时,此 EIP 引入:(i)一个额外的字节 (0x00) 被附加到委托代码的末尾,以及 (ii) 委托代码长度变为 24 字节(例如,EXTCODESIZE 将返回 24)。

这些更改不是破坏性的。但是,它们要求协议、应用程序和合约实现使用严格的偏移量来正确解析委托地址。实现还必须检查委托代码前缀 0xef0100 以确定它是否表示具有委托代码的 EOA,同时避免过度限制的检查,例如断言代码长度必须正好为 23

测试用例

# Initialize the state database and precompiled contract
# 初始化状态数据库和预编译合约
state_db = StateDB()
precompile = PrecompiledContract()

# Test 1: Valid activation and deactivation
# 测试 1:有效的激活和停用
caller = "0x0123"
delegated_addr = bytes.fromhex("1122334455667788990011223344556677889900")
active_code = PrecompiledContract.DELEGATED_CODE_PREFIX + delegated_addr # Active state # 激活状态

state_db.set_code(caller, active_code)
error, gas_left = precompile.execute(caller, state_db, gas=15000)
assert error == b""
assert state_db.get_code(caller) == active_code + b"\x00" # Deactivated state # 停用状态
assert gas_left == 15000 - PrecompiledContract.PRECOMPILE_GAS_COST

error, gas_left = precompile.execute(caller, state_db, gas=15000)
assert error == b""
assert state_db.get_code(caller) == active_code # Turns to the active state again # 再次变为激活状态
assert gas_left == 15000 - PrecompiledContract.PRECOMPILE_GAS_COST

# Test 2: Error cases
# 测试 2:错误情况
error, gas_left = precompile.execute(caller, state_db, gas=15000, read_only=True)
assert error == b"STATICCALL disallows state modification"
assert gas_left == 0

error, gas_left = precompile.execute(caller, state_db, gas=PrecompiledContract.PRECOMPILE_GAS_COST-1)
assert error == b"insufficient gas"
assert gas_left == 0

caller = "0x4567" # EOA that is not delegated to code # 未委托给代码的 EOA
error, gas_left = precompile.execute(caller, state_db, gas=15000)
assert error == b"the address is not an EOA delegated to code"
assert gas_left == 0

caller = "0x89ab" # This is a contract address. # 这是一个合约地址。
state_db.set_code(caller, bytes.fromhex("60006000f3"))
error, gas_left = precompile.execute(caller, state_db, gas=15000)
assert error == b"the address is not an EOA delegated to code"
assert gas_left == 0

参考实现

class PrecompiledContract:
    DELEGATED_CODE_PREFIX = bytes.fromhex("ef0100")
    PRECOMPILE_GAS_COST = 13000

    def execute(self, caller, state_db, gas, read_only=False):
        """
        Switch the private key state of delegated EOAs between active and deactivated.
        在委托 EOA 的活动和停用状态之间切换私钥状态。

        Parameters:
        - caller: The address calling the contract
        - caller: 调用合约的地址
        - state_db: The state database
        - state_db: 状态数据库
        - gas: Gas provided for execution
        - gas: 为执行提供的 Gas
        - read_only: Whether called in a read-only context
        - read_only: 是否在只读上下文中调用

        Returns:
        - Tuple of (result, gas_left)
        - (结果,剩余 gas) 的元组
          result: error bytes on failure, empty bytes on success
          result: 失败时的错误字节,成功时的空字节
          gas_left: remaining gas, 0 on error
          gas_left: 剩余 gas,错误时为 0
        """
        # Check gas
        # 检查 gas
        if gas < self.PRECOMPILE_GAS_COST:
            return b"insufficient gas", 0

        # Check STATICCALL
        # 检查 STATICCALL
        if read_only:
            return b"STATICCALL disallows state modification", 0

        # Get and validate caller's code
        # 获取并验证调用者的代码
        code = state_db.get_code(caller)
        if not code.startswith(self.DELEGATED_CODE_PREFIX):
            return b"the address is not an EOA delegated to code", 0

        # Update delegated code based on length
        # 根据长度更新委托代码
        if len(code) == 23: # Active state # 激活状态
            state_db.set_code(caller, code + b"\x00") # Deactivate # 停用
        elif len(code) == 24: # Deactivated state # 停用状态
            state_db.set_code(caller, code[:-1]) # Activate # 激活
        else: # Although this is not possible, it's added for completeness # 虽然这不可能,但为了完整性而添加
            return b"invalid delegated code length", 0

        return b"", gas - self.PRECOMPILE_GAS_COST

class StateDB:
    """Simplified state database, omitting other account fields"""
    """简化的状态数据库,省略了其他账户字段"""
    def __init__(self):
        self.accounts = {}

    def get_code(self, addr):
        return self.accounts.get(addr, {}).get("code", b"")

    def set_code(self, addr, value):
        if addr not in self.accounts:
            self.accounts[addr] = {}
        self.accounts[addr]["code"] = value

安全注意事项

使用 ECDSA Secp256k1 签名的合约

已经部署并使用 ECDSA secp256k1 签名在交易签名之外的合约(例如,支持 ERC-2612ERC-20 代币,permit 函数)将无法验证 EOA 的停用状态。这意味着私钥签名的签名在这些函数中仍然有效。

为了处理停用的 EOA,新的或可升级的合约可以检查签名地址的停用状态,例如:

  • 使用 EXTCODESIZE 获取账户的代码长度。
  • 如果代码长度为 24 字节,则使用 EXTCODECOPY 验证代码是否以 0xef0100(委托代码前缀)开头。
  • 如果同时满足两个条件(即,代码大小为 24 字节且代码以 0xef0100 开头),则确认私钥已停用,应拒绝签名。

对于不可升级的合约,上述方法无法直接应用。另一种潜在的解决方案,在协议级别,是修改 ecRecover 预编译合约:如果恢复地址的私钥已停用,则 ecRecover 预编译合约可以返回预编译合约错误(或者,如果不添加错误返回路径,则返回零地址或抗碰撞地址,例如 0x1)。但是,这种方法也有局限性,因为它不能涵盖合约实现自己的签名验证逻辑而不依赖于 ecRecover 的情况。

不可逆的停用

委托给缺乏重新激活支持的钱包(例如,通过适当的接口调用预编译合约)可能导致不可逆的停用。为了减轻这种风险,用户应仅将其 EOA 委托给经过彻底审计并明确支持此 EIP 的实现。

停用和重新激活重放

重放攻击可能发生在两种情况下:(i)在单个链上重复相同的授权,或(ii)在不同链上使用授权。

对于通过 EOA 签名交易进行的停用,交易中 nonces 的使用确保了同一消息不能在同一链上多次重放。此外,EIP-155 提供的重放保护机制(如果启用)有效地阻止了跨链消息重放。

对于通过委托合约进行的停用或重新激活,合约应实施强大的重放保护机制(例如,将自定义 nonce 和链 ID 添加到签名消息)以防止在同一链和不同链上的重放攻击。特别是当 EOA 已经(或将要)委托给多个链上的相同实现时。

版权

版权及相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Liyi Guo (@colinlyguo), "EIP-7851: 停用/重新激活委托 EOA 的密钥 [DRAFT]," Ethereum Improvement Proposals, no. 7851, December 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7851.