Alert Source Discuss
Standards Track: ERC

ERC-6538: 隐身元地址注册表

一个规范的合约,供实体直接或通过使用签名的第三方注册隐身元地址。

Authors Matt Solomon (@mds1), Toni Wahrstätter (@nerolation), Ben DiFrancesco (@apbendi), Vitalik Buterin (@vbuterin), Gary Ghayrat (@garyghayrat)
Created 2023-01-24
Requires EIP-712, EIP-1271, EIP-5564

摘要

本规范定义了一种存储和检索实体隐身元地址的标准化方法,通过扩展 ERC-5564。实体可以直接注册他们的隐身元地址。第三方也可以使用有效的 EIP-712EIP-1271 签名代表实体进行注册。注册后,任何智能合约或用户都可以检索该实体的隐身元地址。可以将隐身元地址与 ERC-5564 中指定的 generateStealthAddress 一起使用,以将资产发送到生成的隐身地址,而无需透露实体的地址。

动机

隐身地址生成的标准化有可能通过允许转移的接收者在接收资产时保持匿名来大大增强以太坊的隐私能力。通过引入一个中央智能合约供用户存储他们的隐身元地址,EOA 和合约可以使用各种隐身地址方案以编程方式进行隐身交互。

规范

本文档中的关键词“必须(MUST)”、“禁止(MUST NOT)”、“必需(REQUIRED)”、“应(SHALL)”、“不应(SHALL NOT)”、“应该(SHOULD)”、“不应该(SHOULD NOT)”、“推荐(RECOMMENDED)”、“可以(MAY)”和“可选(OPTIONAL)”应按照 RFC 2119 中的描述进行解释。

此合约定义了一个 ERC6538Registry,用于存储实体的隐身元地址。这些实体可以通过地址、ENS 名称或其他标识符来识别。这必须是一个单例合约,每个链一个实例。

合约规范如下。使用一个字节的整数来标识隐身地址方案。此整数用于区分不同的隐身地址方案。此 ERC 概述了方案 ID 1 作为具有视图标签的 SECP256k1 曲线加密方案,如 ERC-5564 中所指定。

// SPDX-License-Identifier: CC0-1.0
pragma solidity 0.8.23;

/// @notice `ERC6538Registry` 合约,用于将帐户映射到其隐身元地址。请参阅
/// [ERC-6538](https://eips.ethereum.org/EIPS/eip-6538) 了解更多信息。
contract ERC6538Registry {
  /// @notice 当提供给 `registerKeysOnBehalf` 的签名无效时发出。
  error ERC6538Registry__InvalidSignature();

  /// @notice 从 `user` 期望的下一个 nonce,用于为 `registerKeysOnBehalf` 进行签名。
  /// @dev `registrant` 可以是标准的 160 位地址或任何其他标识符。
  /// @dev `schemeId` 是隐身地址方案的整数标识符。
  mapping(address registrant => mapping(uint256 schemeId => bytes)) public stealthMetaAddressOf;

  /// @notice 用于确保签名只能使用一次的 nonce。
  /// @dev `registrant` 是用户地址。
  /// @dev 每次有效的 `registerKeysOnBehalf` 调用后,`nonce` 将递增。
  mapping(address registrant => uint256) public nonceOf;

  /// @notice 在 `registerKeysOnBehalf` 中使用的 EIP-712 类型哈希。
  bytes32 public constant ERC6538REGISTRY_ENTRY_TYPE_HASH =
    keccak256("Erc6538RegistryEntry(uint256 schemeId,bytes stealthMetaAddress,uint256 nonce)");

  /// @notice 此合约最初部署的链 ID。
  uint256 internal immutable INITIAL_CHAIN_ID;

  /// @notice 此合约中使用的域分隔符。
  bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR;

  /// @notice 当注册者更新其隐身元地址时发出。
  /// @param registrant 注册隐身元地址的帐户。
  /// @param schemeId 对应于应用的隐身地址方案的标识符,例如,ERC-5564 中指定的 secp256k1 为 1。
  /// @param stealthMetaAddress 隐身元地址。
  /// [ERC-5564](https://eips.ethereum.org/EIPS/eip-5564) 基于
  /// [ERC-3770](https://eips.ethereum.org/EIPS/eip-3770) 设置了隐身元地址的格式,并将其指定为:
  ///   st:<shortName>:0x<spendingPubKey>:<viewingPubKey>
  /// 链 (`shortName`) 基于部署 `ERC6538Registry` 的链是隐式的,
  /// 因此,此 `stealthMetaAddress` 只是压缩的 `spendingPubKey` 和
  /// `viewingPubKey` 的串联。
  event StealthMetaAddressSet(
    address indexed registrant, uint256 indexed schemeId, bytes stealthMetaAddress
  );

  /// @notice 当注册者递增其 nonce 时发出。
  /// @param registrant 递增 nonce 的帐户。
  /// @param newNonce 新的 nonce 值。
  event NonceIncremented(address indexed registrant, uint256 newNonce);

  constructor() {
    INITIAL_CHAIN_ID = block.chainid;
    INITIAL_DOMAIN_SEPARATOR = _computeDomainSeparator();
  }

  /// @notice 为给定的方案 ID 设置调用者的隐身元地址。
  /// @param schemeId 对应于应用的隐身地址方案的标识符,例如,ERC-5564 中指定的 secp256k1 为 1。
  /// @param stealthMetaAddress 要注册的隐身元地址。
  function registerKeys(uint256 schemeId, bytes calldata stealthMetaAddress) external {
    stealthMetaAddressOf[msg.sender][schemeId] = stealthMetaAddress;
    emit StealthMetaAddressSet(msg.sender, schemeId, stealthMetaAddress);
  }

  /// @notice 为给定的方案 ID 设置 `registrant` 的隐身元地址。
  /// @param registrant 注册者的地址。
  /// @param schemeId 对应于应用的隐身地址方案的标识符,例如,ERC-5564 中指定的 secp256k1 为 1。
  /// @param signature 来自 `registrant` 的授权注册的签名。
  /// @param stealthMetaAddress 要注册的隐身元地址。
  /// @dev 支持 EOA 签名和 EIP-1271 签名。
  /// @dev 如果签名无效,则恢复。
  function registerKeysOnBehalf(
    address registrant,
    uint256 schemeId,
    bytes memory signature,
    bytes calldata stealthMetaAddress
  ) external {
    bytes32 dataHash;
    address recoveredAddress;

    unchecked {
      dataHash = keccak256(
        abi.encodePacked(
          "\x19\x01",
          DOMAIN_SEPARATOR(),
          keccak256(
            abi.encode(
              ERC6538REGISTRY_ENTRY_TYPE_HASH,
              schemeId,
              keccak256(stealthMetaAddress),
              nonceOf[registrant]++
            )
          )
        )
      );
    }

    if (signature.length == 65) {
      bytes32 r;
      bytes32 s;
      uint8 v;
      assembly ("memory-safe") {
        r := mload(add(signature, 0x20))
        s := mload(add(signature, 0x40))
        v := byte(0, mload(add(signature, 0x60)))
      }
      recoveredAddress = ecrecover(dataHash, v, r, s);
    }

    if (
      (
        (recoveredAddress == address(0) || recoveredAddress != registrant)
          && (
            IERC1271(registrant).isValidSignature(dataHash, signature)
              != IERC1271.isValidSignature.selector
          )
      )
    ) revert ERC6538Registry__InvalidSignature();

    stealthMetaAddressOf[registrant][schemeId] = stealthMetaAddress;
    emit StealthMetaAddressSet(registrant, schemeId, stealthMetaAddress);
  }

  /// @notice 递增发送者的 nonce 以使现有签名无效。
  function incrementNonce() external {
    unchecked {
      nonceOf[msg.sender]++;
    }
    emit NonceIncremented(msg.sender, nonceOf[msg.sender]);
  }

  /// @notice 返回此合约中使用的域分隔符。
  /// @dev 如果存在链分叉,则重新计算域分隔符。
  function DOMAIN_SEPARATOR() public view returns (bytes32) {
    return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : _computeDomainSeparator();
  }

  /// @notice 计算此合约的域分隔符。
  function _computeDomainSeparator() internal view returns (bytes32) {
    return keccak256(
      abi.encode(
        keccak256(
          "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
        ),
        keccak256("ERC6538Registry"),
        keccak256("1.0"),
        block.chainid,
        address(this)
      )
    );
  }
}

/// @notice ERC1271 标准签名验证方法的接口,用于合约,如 https://eips.ethereum.org/EIPS/eip-1271[ERC-1271] 中定义。
interface IERC1271 {
  /// @notice 应该返回为提供的数据提供的签名是否有效
  /// @param hash 要签名的数据的哈希
  /// @param signature 与 _data 关联的签名字节数组
  function isValidSignature(bytes32 hash, bytes memory signature)
    external
    view
    returns (bytes4 magicValue);
}

此合约的接口定义如下:

// SPDX-License-Identifier: CC0-1.0
pragma solidity 0.8.23;

/// @dev 用于调用 `ERC6538Registry` 合约以将帐户映射到其隐身元地址的接口。请参阅 [ERC-6538](https://eips.ethereum.org/EIPS/eip-6538) 了解更多信息。
interface IERC6538Registry {
  /// @notice 当提供给 `registerKeysOnBehalf` 的签名无效时发出。
  error ERC6538Registry__InvalidSignature();

  /// @dev 当注册者更新其隐身元地址时发出。
  /// @param registrant 注册隐身元地址的帐户。
  /// @param schemeId 对应于应用的隐身地址方案的标识符,例如,ERC-5564 中指定的 secp256k1 为 1。
  /// @param stealthMetaAddress 隐身元地址。
  /// [ERC-5564](https://eips.ethereum.org/EIPS/eip-5564) 基于
  /// [ERC-3770](https://eips.ethereum.org/EIPS/eip-3770) 设置了隐身元地址的格式,并将其指定为:
  ///   st:<shortName>:0x<spendingPubKey>:<viewingPubKey>
  /// 链 (`shortName`) 基于部署 `ERC6538Registry` 的链是隐式的,
  /// 因此,此 `stealthMetaAddress` 只是 `spendingPubKey` 和 `viewingPubKey` 的串联。
  event StealthMetaAddressSet(
    address indexed registrant, uint256 indexed schemeId, bytes stealthMetaAddress
  );

  /// @notice 当注册者递增其 nonce 时发出。
  /// @param registrant 递增 nonce 的帐户。
  /// @param newNonce 新的 nonce 值。
  event NonceIncremented(address indexed registrant, uint256 newNonce);

  /// @notice 为给定的方案 ID 设置调用者的隐身元地址。
  /// @param schemeId 对应于应用的隐身地址方案的标识符,例如,ERC-5564 中指定的 secp256k1 为 1。
  /// @param stealthMetaAddress 要注册的隐身元地址。
  function registerKeys(uint256 schemeId, bytes calldata stealthMetaAddress) external;

  /// @notice 为给定的方案 ID 设置 `registrant` 的隐身元地址。
  /// @param registrant 注册者的地址。
  /// @param schemeId 对应于应用的隐身地址方案的标识符,例如,ERC-5564 中指定的 secp256k1 为 1。
  /// @param signature 来自 `registrant` 的授权注册的签名。
  /// @param stealthMetaAddress 要注册的隐身元地址。
  /// @dev 支持 EOA 签名和 EIP-1271 签名。
  /// @dev 如果签名无效,则恢复。
  function registerKeysOnBehalf(
    address registrant,
    uint256 schemeId,
    bytes memory signature,
    bytes calldata stealthMetaAddress
  ) external;

  /// @notice 递增发送者的 nonce 以使现有签名无效。
  function incrementNonce() external;

  /// @notice 返回此合约中使用的域分隔符。
  function DOMAIN_SEPARATOR() external view returns (bytes32);

  /// @notice 返回给定 `registrant` 和 `schemeId` 的隐身元地址。
  function stealthMetaAddressOf(address registrant, uint256 schemeId)
    external
    view
    returns (bytes memory);

  /// @notice 返回在 `registerKeysOnBehalf` 中使用的 EIP-712 类型哈希。
  function ERC6538REGISTRY_ENTRY_TYPE_HASH() external view returns (bytes32);

  /// @notice 返回给定 `registrant` 的 nonce。
  function nonceOf(address registrant) external view returns (uint256);
}

部署方法

ERC6538Registry 合约部署在 0x6538E6bf4B0eBd30A8Ea093027Ac2422ce5d6538,使用 CREATE2 通过确定性部署器位于 0x4e59b44847b379578588920ca78fbf26c0b4956c,盐值为 0x7cac4e512b1768c627c9e711c7a013f1ad0766ef5125c59fb7161dade58da078

理由

拥有一个用于注册隐身元地址的中央智能合约有以下几个好处:

  1. 它保证了与其他智能合约的互操作性,因为它们可以轻松检索和利用注册的隐身元地址。这使得 ENS 或 Gnosis Safe 等应用程序可以使用该信息并将隐身地址集成到他们的服务中。

  2. 它确保用户不依赖链下资源来检索用户的隐身元地址。

  3. 在此合约中注册隐身元地址为用户提供了一种标准方式来传达他们已准备好参与隐身交互。

  4. 通过将注册表部署为单例合约,多个项目可以访问同一组隐身元地址,从而有助于改进标准化。

向后兼容性

此 EIP 完全向后兼容。

参考实现

你可以在此处找到 ERC6538Registry 合约的实现,并在此处找到接口 IERC6538Registry.sol

安全注意事项

如果私钥泄露,注册人应立即从隐身密钥注册表中取消注册,以防止未来发送到泄露帐户的资金损失。

版权

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

Citation

Please cite this document as:

Matt Solomon (@mds1), Toni Wahrstätter (@nerolation), Ben DiFrancesco (@apbendi), Vitalik Buterin (@vbuterin), Gary Ghayrat (@garyghayrat), "ERC-6538: 隐身元地址注册表," Ethereum Improvement Proposals, no. 6538, January 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6538.