Alert Source Discuss
🚧 Stagnant Standards Track: ERC

ERC-5559: 跨链写入延迟协议

跨链写入延迟协议提供了一种机制,可以将改变的存储和解析延迟到链下处理程序

Authors Paul Gauvreau (@0xpaulio), Nick Johnson (@arachnid)
Created 2022-06-23
Discussion Link https://ethereum-magicians.org/t/eip-cross-chain-write-deferral-protocol/10576
Requires EIP-712

摘要

以下标准提供了一种机制,智能合约可以通过该机制请求外部处理程序来解析各种任务。 这提供了一种机制,协议可以通过将数据的处理延迟到另一个系统/网络来减少与在主网上存储数据相关的 gas 费用。 这些外部处理程序充当核心 L1 合约的扩展。

本标准概述了一组可用于管理突变(任务)的执行和存储的处理程序类型,以及它们相应的权衡。 每种处理程序类型都有相关的运营成本、最终性保证和去中心化程度。 通过进一步指定突变被推迟到的处理程序的类型,协议可以更好地定义如何授权和保护其系统。

该标准可以与 EIP-3668 结合使用,以提供一种机制,协议可以通过主网上的 L1 合约驻留并与之交互,同时能够解析和改变存储在外部系统中的数据。

动机

EIP-3668 提供了一种机制,可以通过透明的方式在智能合约中定义链下查找。 此外,它还提供了一种方案,可以在链上验证解析后的数据。 然而,缺少一个标准,据此可以通过原生合约请求对链下数据执行的突变。 此外,随着 L2 解决方案的增加,智能合约工程师拥有额外的工具,可用于降低在以太坊主网上执行突变的存储和交易成本。

一种允许智能合约将数据的存储和解析延迟到外部处理程序的规范有助于编写与所使用的存储解决方案无关的客户端,从而实现无需了解与其交互的合约相关的底层处理程序即可运行的新应用程序。

这方面的例子包括:

  • 允许像管理原生 L1 代币一样,管理在 L2 解决方案或链下数据库上外部解析的 ENS 域名。
  • 允许像管理存储在原生 L1 智能合约中的数字身份一样,管理存储在外部处理程序中的数字身份。

规范

概述

主要有两种处理程序分类:L2 合约和链下数据库。 这些是根据处理程序的部署位置确定的。 处理程序分类用于更好地定义与其部署相关的不同安全保证和要求。

从高层次来看:

  • 托管在 L2 解决方案上的处理程序与 EVM 兼容,并且可以使用以太坊生态系统原生的属性(例如地址)来授权访问。
  • 托管在链下数据库上的处理程序需要额外的参数和签名才能正确执行身份验证并检查请求的有效性。

延迟突变可以在短短两个步骤内处理。 但是,在某些情况下,突变可能会被延迟多次。

  1. 查询或向合约发送交易
  2. 使用步骤 1 中提供的参数查询或向处理程序发送交易

在步骤 1 中,对合约进行标准区块链调用操作。 合约按预期执行操作,或者返回一个错误,该错误指定突变被延迟到的处理程序的类型以及执行后续突变所需的相应参数。 合约可以返回两种类型的错误,但其他 EIP 中可能会定义更多错误:

  • StorageHandledByL2(chainId, contractAddress)
  • StorageHandledByOffChainDatabase(sender, url, data)

在步骤 2 中,客户端根据 (1) 中收到的错误类型构建并执行新请求。 这些握手在以下各节中概述:

在某些情况下,突变可能会被延迟多次

数据存储在 L1 中

┌──────┐                ┌───────────┐ 
│Client│                │L1 Contract│ 
└──┬───┘                └─────┬─────┘ 
   │                          │       
   │ somefunc(...)            │       
   ├─────────────────────────►│       
   │                          │       
   │ response                 │       
   │◄─────────────────────────┤       
   │                          │       

如果未发生返回,则在执行交易时,数据将存储在 L1 合约中。

数据存储在 L2 中

┌──────┐                                           ┌───────────┐  ┌─────────────┐
│Client│                                           │L1 Contract│  │ L2 Contract │
└──┬───┘                                           └─────┬─────┘  └──────┬──────┘
   │                                                     │               │       
   │ somefunc(...)                                       │               │       
   ├────────────────────────────────────────────────────►│               │       
   │                                                     │               │       
   │ revert StorageHandledByL2(chainId, contractAddress) │               │       
   │◄────────────────────────────────────────────────────┤               │       
   │                                                     │               │       
   │ Execute Tx [chainId] [contractAddress] [callData]   │               │       
   ├─────────────────────────────────────────────────────┼──────────────►│       
   │                                                     │               │       
   │ response                                            │               │       
   │◄────────────────────────────────────────────────────┼───────────────┤       
   │                                                     │               │       

对 L1 合约的调用或交易返回 StorageHandledByL2(chainId, contractAddress) 错误。

在这种情况下,客户端为 contractAddress 构建一个新交易,其中包含原始 callData,并将其发送到他们选择的 RPC,以获取相应的 chainIdchainId 参数对应于与 EVM 兼容的 L2 解决方案。

示例

假设合约具有以下方法:

function setAddr(bytes32 node, address a) external;

此突变的数据存储在与 EVM 兼容的 L2 上并进行跟踪。 合约作者希望减少与合约相关的 gas 费用,同时保持协议的互操作性和去中心化。 因此,该突变通过返回 StorageHandledByL2(chainId, contractAddress) 错误来延迟到链下处理程序。

setAddr 的一个有效实现示例如下:

function setAddr(bytes32 node, address a) external {
   revert StorageHandledByL2(
      10,
      _l2HandlerContractAddress
   ); 
}

例如,如果合约在 StorageHandledByL2 中返回以下数据:

chainId = 10
contractAddress = 0x0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff

收到此错误的用户为相应的 chainId 创建一个新交易,并构建一个包含原始 callData 的交易以发送到 contractAddress。 用户必须选择他们选择的 RPC,以便将交易发送到相应的 chainId

数据存储在链下数据库中

┌──────┐                                           ┌───────────┐  ┌────────────────────┐
│Client│                                           │L1 Contract│  │ Off-Chain Database │
└──┬───┘                                           └─────┬─────┘  └──────────┬─────────┘
   │                                                     │                   │ 
   │ somefunc(...)                                       │                   │ 
   ├────────────────────────────────────────────────────►│                   │ 
   │                                                     │                   │ 
   │ revert StorageHandledByOffChainDatabase(sender,     |                   │ 
   │                               urls, requestParams)  │                   │ 
   │◄────────────────────────────────────────────────────┤                   │ 
   │                                                     │                   │ 
   │ HTTP Request [requestParams, signature]             │                   │ 
   ├─────────────────────────────────────────────────────┼──────────────────►│ 
   │                                                     │                   │ 
   │ response                                            │                   │ 
   │◄────────────────────────────────────────────────────┼───────────────────┤ 
   │                                                     │                   │ 

对 L1 合约的调用或交易返回 StorageHandledByOffChainDatabase(sender, url, data) 错误。

在这种情况下,客户端对网关服务执行 HTTP POST 请求。 网关服务由 url 定义。 附加到请求的主体是一个 JSON 对象,其中包含 senderdatadata 的签名副本,表示为 signature。 签名是根据 EIP-712 生成的,其中使用域定义、sender 和消息上下文 data 生成类型化数据签名。

sender 是一个 ABI 编码的结构体,定义为:

/**
* @notice 用于定义 EIP-712 中定义的类型化数据签名的域的结构体。
* @param name 签名对应的合约的用户友好名称。
* @param version 正在使用的域对象的版本。
* @param chainId 签名对应的链的 ID(即以太坊主网:1,Goerli 测试网:5,...)。
* @param verifyingContract 签名所属的合约的地址。
*/
struct domainData {
    string name;
    string version;
    uint64 chainId;
    address verifyingContract;
}    

data 是一个 abi 编码的结构体,定义为:

/**
* @notice 用于定义用于构造 EIP-712 中定义的类型化数据签名的消息上下文的结构体,
* 以授权和定义正在执行的延迟突变。
* @param functionSelector 相应突变的功能选择器。
* @param sender 执行突变的用户的地址 (msg.sender)。
* @param parameter[] 定义用于执行延迟突变的输入的 <key, value> 对列表。
*/
struct messageData {
    bytes4 functionSelector;
    address sender;
    parameter[] parameters;
    uint256 expirationTimestamp;
}

/**
* @notice 用于定义链下数据库处理程序推迟的参数的结构体。
* @param name 参数的变量名称。
* @param value 参数的字符串编码值表示形式。
*/
struct parameter {
    string name;
    string value;
}

signature 是通过使用 senderdata 参数构造 EIP-712 类型化数据签名生成的。

HTTP POST 请求中使用的主体定义为:

{
    "sender": "<abi encoded domainData (sender)>",
    "data": "<abi encoded messageData (data)>",
    "signature": "<EIP-712 typed data signature of corresponding message data & domain definition>"
}

示例

假设合约具有以下方法:

function setAddr(bytes32 node, address a) external;

此突变的数据存储在某种链下数据库中并进行跟踪。 合约作者希望用户能够授权并修改他们的 Addr,而无需支付 gas 费用。 因此,该突变通过返回 StorageHandledByOffChainDatabase(sender, url, data) 错误来延迟到链下处理程序。

setAddr 的一个有效实现示例如下:

function setAddr(bytes32 node, address a) external {
    IWriteDeferral.parameter[] memory params = new IWriteDeferral.parameter[](3);

    params[0].name = "node";
    params[0].value = BytesToString.bytes32ToString(node);

    params[1].name = "coin_type";
    params[1].value = Strings.toString(coinType);

    params[2].name = "address";
    params[2].value = BytesToString.bytesToString(a);

    revert StorageHandledByOffChainDatabase(
        IWriteDeferral.domainData(
            {
                name: WRITE_DEFERRAL_DOMAIN_NAME,
                version: WRITE_DEFERRAL_DOMAIN_VERSION,
                chainId: 1,
                verifyingContract: address(this)
            }
        ),
        _offChainDatabaseUrl,
        IWriteDeferral.messageData(
            {
                functionSelector: msg.sig,
                sender: msg.sender,
                parameters: params,
                expirationTimestamp: block.timestamp + _offChainDatabaseTimeoutDuration
            }
        )
    );
}

例如,如果合约返回以下内容:

StorageHandledByOffChainDatabase(
    (
        "CoinbaseResolver", 
        "1", 
        1, 
        0x32f94e75cde5fa48b6469323742e6004d701409b
    ), 
    "https://example.com/r/{sender}", 
    (
        0xd5fa2b00, 
        0x727f366727d3c9cc87f05d549ee2068f254b267c, 
        [
            ("node", "0x418ae76a9d04818c7a8001095ad01a78b9cd173ee66fe33af2d289b5dc5f4cba"), 
            ("coin_type", "60"), 
            ("address", "0x727f366727d3c9cc87f05d549ee2068f254b267c")
        ], 
        181
    )
)

收到此错误的用户构造类型化数据签名,对其进行签名,并通过 HTTP POST 对 url 执行该请求。

包含 requestParamssignature 的示例 HTTP POST 请求正文:

{
    "sender": "<abi encoded domainData (sender)>",
    "data": "<abi encoded messageData (data)>",
    "signature": "<EIP-712 typed data signature of corresponding message data & domain definition>"
}

请注意,消息可以在签名和请求之前以任何方式、形状或形式进行更改。 后端有责任正确授权和处理这些突变。 从安全角度来看,这与用户能够使用他们想要的任何参数调用智能合约没有什么不同,因为智能合约有责任授权和处理这些请求。

数据存储在 L2 和链下数据库中

┌──────┐                                           ┌───────────┐  ┌─────────────┐  ┌────────────────────┐
│Client│                                           │L1 Contract│  │ L2 Contract │  │ Off-Chain Database │
└──┬───┘                                           └─────┬─────┘  └──────┬──────┘  └──────────┬─────────┘
   │                                                     │               │                    │
   │ somefunc(...)                                       │               │                    │
   ├────────────────────────────────────────────────────►│               │                    │
   │                                                     │               │                    │
   │ revert StorageHandledByL2(chainId, contractAddress) │               │                    │
   │◄────────────────────────────────────────────────────┤               │                    │
   │                                                     │               │                    │
   │ Execute Tx [chainId] [contractAddress] [callData]   │               │                    │
   ├─────────────────────────────────────────────────────┼──────────────►│                    │
   │                                                     │               │                    │
   │ revert StorageHandledByOffChainDatabase(sender, url, data)          │                    │
   │◄────────────────────────────────────────────────────┼───────────────┤                    │
   │                                                     │               │                    │
   │ HTTP Request {requestParams, signature}             │               │                    │
   ├─────────────────────────────────────────────────────┼───────────────┼───────────────────►│
   │                                                     │               │                    │
   │ response                                            │               │                    │
   │◄────────────────────────────────────────────────────┼───────────────┼────────────────────┤
   │                                                     │               │                    │

对 L1 合约的调用或交易返回 StorageHandledByL2(chainId, contractAddress) 错误。

在这种情况下,客户端为 contractAddress 构建一个新交易,其中包含原始 callData,并将其发送到他们选择的 RPC 以获取相应的 chainId

然后,对 L2 合约的调用或交易返回 StorageHandledByOffChainDatabase(sender, url, data) 错误。

在这种情况下,客户端然后对网关服务执行 HTTP POST 请求。 网关服务由 url 定义。 附加到请求的主体是一个 JSON 对象,其中包含 senderdatasignature——对应于 EIP-712 的类型化数据签名。

事件

在更改处理程序的核心变量时,必须发出相应的事件。 这增加了与不同管理操作相关的透明度。 核心变量包括 L2 解决方案的 chainIdcontractAddress 以及链下数据库解决方案的 url。 这些事件在下面的 WriteDeferral 接口中概述。

写入延迟接口

下面是一个基本接口,它定义并描述了所有返回类型及其对应的参数。

pragma solidity ^0.8.13;

interface IWriteDeferral {
    /*//////////////////////////////////////////////////////////////
                                 EVENTS
    //////////////////////////////////////////////////////////////*/

    /// @notice 当相应 L2 处理程序的默认 chainId 更改时引发的事件。
    event L2HandlerDefaultChainIdChanged(uint256 indexed previousChainId, uint256 indexed newChainId);
    /// @notice 当与 chainId 对应的 L2 处理程序的 contractAddress 更改时引发的事件。
    event L2HandlerContractAddressChanged(uint256 indexed chainId, address indexed previousContractAddress, address indexed newContractAddress);

    /// @notice 当相应链下数据库处理程序的 URL 更改时引发的事件。
    event OffChainDatabaseHandlerURLChanged(string indexed previousUrl, string indexed newUrl);

    /*//////////////////////////////////////////////////////////////
                                 STRUCTS
    //////////////////////////////////////////////////////////////*/

    /**
     * @notice 用于定义 EIP-712 中定义的类型化数据签名的域的结构体。
     * @param name 签名对应的合约的用户友好名称。
     * @param version 正在使用的域对象的版本。
     * @param chainId 签名对应的链的 ID(即以太坊主网:1,Goerli 测试网:5,...)。
     * @param verifyingContract 签名所属的合约的地址。
     */
    struct domainData {
        string name;
        string version;
        uint64 chainId;
        address verifyingContract;
    }    

    /**
     * @notice 用于定义用于构造 EIP-712 中定义的类型化数据签名的消息上下文的结构体,
     * 以授权和定义正在执行的延迟突变。
     * @param functionSelector 相应突变的功能选择器。
     * @param sender 执行突变的用户的地址 (msg.sender)。
     * @param parameter[] 定义用于执行延迟突变的输入的 <key, value> 对列表。
     */
    struct messageData {
        bytes4 functionSelector;
        address sender;
        parameter[] parameters;
        uint256 expirationTimestamp;
    }

    /**
     * @notice 用于定义链下数据库处理程序推迟的参数的结构体。
     * @param name 参数的变量名称。
     * @param value 参数的字符串编码值表示形式。
     */
    struct parameter {
        string name;
        string value;
    }


    /*//////////////////////////////////////////////////////////////
                                 ERRORS
    //////////////////////////////////////////////////////////////*/

    /**
     * @dev 当突变被延迟到 L2 时要引发的错误。
     * @param chainId 要对其执行延迟突变的链 ID。
     * @param contractAddress 延迟突变应与之交易的合约地址。
     */
    error StorageHandledByL2(
        uint256 chainId, 
        address contractAddress
    );

    /**
     * @dev 当突变被延迟到链下数据库时要引发的错误。
     * @param sender 是执行链下数据库的相应合约的 EIP-712 域定义,写入
     * 延迟返回。
     * @param url 用于请求执行链下突变的 URL。
     * @param data 用于授权和指示延迟到
     * 链下数据库处理程序的突变的 EIP-712 消息签名数据上下文。
     * 为了授权要执行的延迟突变,用户必须使用域定义 (sender) 和消息数据
     * (data) 来构造 EIP-712 中定义的类型数据签名请求。 此签名、消息数据 (data) 和 domainData (sender)
     * 然后包含在 HTTP POST 请求中,表示为 sender、data 和 signature。
     * 
     * 示例 HTTP POST 请求:
     *  {
     *      "sender": <abi encoded domainData (sender)>,
     *      "data": <abi encoded message data (data)>,
     *      "signature": <corresponding message data & domain definition 的 EIP-712 typed data signature>
     *  }
     * 
     */
    error StorageHandledByOffChainDatabase(
        domainData sender, 
        string url, 
        messageData data
    );     
}

将交易与存储延迟返回结合使用

在某些情况下,合约可能会有条件地延迟和处理突变,在这种情况下,可能需要交易。 使用此方法发送可能导致延迟返回的交易很简单,因为客户端在 preflighting 交易时应收到相应的返回。

此功能非常适合希望允许其用户定义与其操作相关的安全保证和成本的应用程序。 例如,在去中心化身份配置文件的情况下,用户可能不关心其数据是否去中心化,而是选择将其记录的处理延迟到链下处理程序,以减少 gas 费用和链上交易。

理由

使用 revert 传递调用信息

EIP-3668 采用了使用 revert 传递调用信息的想法。 它被提出作为一种简单的机制,可以在满足任何预先存在的接口或函数签名的同时,维持一种指示和触发链下查找的机制。

这与本 EIP 中定义的写入延迟协议非常相似; 无需对 ABI 或底层 EVM 进行任何修改,revert 提供了一种干净的机制,我们可以通过该机制“返回”类型化指令 - 以及完成该操作的相应元素 - 而无需修改相应函数的签名。 这使得它易于遵守预先存在的接口和基础设施。

使用多个返回和处理程序类型来更好地定义安全保证

通过进一步定义处理程序的类别,开发人员可以提高粒度,以定义与链下存储数据相关的特征和不同的保证。 此外,不同的处理程序需要不同的参数和验证机制。 这对于协议的透明度非常重要,因为它们将数据存储在原生以太坊生态系统之外。 此协议的常见实现可能包括将非运营数据存储在 L2 解决方案和链下数据库中以减少 gas 费用,同时保持开放的互操作性。

向后兼容性

不希望使用此规范的现有合约不受影响。 客户端可以为所有合约调用添加对跨链写入延迟的支持,而不会引入任何新的开销或不兼容性。

需要跨链写入延迟的合约将无法与未实现此规范的客户端结合使用。 尝试从不兼容的客户端调用这些合约将导致合约抛出异常并传播给用户。

安全考虑事项

延迟的突变永远不应解析为主网以太坊。 这种将突变延迟回 ETH 的尝试可能包括劫持尝试,其中合约开发者试图让用户签名并发送恶意交易。 此外,当交易被延迟到 L2 系统时,它必须使用原始 calldata,这可以防止交易中潜在的恶意上下文更改。

指纹识别攻击

由于所有延迟的突变都将在 data 中包含 msg.sender 参数,因此 StorageHandledByOffChainDatabase 返回可能会指纹识别钱包地址以及用于发出 HTTP 请求的相应 IP 地址。 这的影响特定于应用程序,用户应该了解这是与链下处理程序相关的风险。 为了最大限度地减少这种情况的安全影响,我们提出以下建议:

  1. 智能合约开发者应为用户提供直接在网络上解析数据的选项。 允许他们启用链上存储,使用户可以简单地分析其数据的解析位置的成本效益,以及与解析位置相关的不同保证/风险。
  2. 客户端库应为客户端提供一个钩子,以覆盖跨链写入延迟 StorageHandledByOffChainDatabase 调用 - 可以通过重写它们以使用代理服务,或者完全拒绝它们。 应该编写此机制或另一种机制,以便于将域名添加到允许列表或阻止列表。

我们鼓励应用程序尽可能透明地了解其设置和采取的不同预防措施。

版权

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

Citation

Please cite this document as:

Paul Gauvreau (@0xpaulio), Nick Johnson (@arachnid), "ERC-5559: 跨链写入延迟协议 [DRAFT]," Ethereum Improvement Proposals, no. 5559, June 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5559.