Alert Source Discuss
Standards Track: ERC

ERC-5202: 蓝图合约格式

定义一种字节码容器格式,用于索引和利用蓝图合约

Authors Charles Cooper (@charles-cooper), Edward Amor (@skellet0r)
Created 2022-06-23
Requires EIP-170

摘要

定义“蓝图”合约的标准,即表示存储在链上的 initcode 的合约。

动机

为了减少部署者合约的大小,一个有用的模式是将 initcode 作为“蓝图”合约存储在链上,然后使用 EXTCODECOPYinitcode 复制到内存中,然后调用 CREATECREATE2。但是,这存在以下问题:

  • 外部工具和索引器很难检测合约是“常规”运行时合约还是“蓝图”合约。启发式地搜索字节码中的模式以确定它是否是 initcode 会带来维护和正确性问题。
  • 在链上逐字节存储 initcode 是一个正确性和安全问题。由于 EVM 没有一种原生方法来区分可执行代码和其他类型的代码,除非 initcode 显式地实现 ACL 规则,否则任何人都可以调用这样的“蓝图”合约并直接将 initcode 作为普通运行时代码执行。如果蓝图合约存储的 initcode 具有副作用(例如写入存储或调用外部合约),则尤其成问题。如果蓝图合约存储的 initcode 执行 SELFDESTRUCT 操作码,则甚至可以删除蓝图合约,从而阻止依赖于蓝图存在的下游部署者合约的正确操作。因此,最好在蓝图合约前加上一个特殊的前导码以防止执行。

规范

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

蓝图合约必须使用前导码 0xFE71<version bits><length encoding bits>。6 位分配给版本,2 位分配给长度编码。第一个版本从 0 (0b000000) 开始,版本递增 1。<length encoding bits> 的值 0b11 是保留的。如果长度位是 0b11,则第三个字节被认为是延续字节(即,版本需要多个字节来编码)。多字节版本的精确编码留给未来的 ERC。

蓝图合约必须包含至少一个字节的 initcode

蓝图合约可以在版本字节和 initcode 之间插入任何字节(数据或代码)。如果使用这种可变长度的数据,则前导码必须是 0xFE71<version bits><length encoding bits><length bytes><data><length encoding bits> 表示一个介于 0 和 2(含 0 和 2)之间的数字,描述了 <length bytes> 采用的字节数,而 <length bytes><data> 采用的字节数的 big-endian 编码。

理由

  • 为了节省 gas 和存储空间,前导码应尽可能小。

  • 尝试直接 CALL 蓝图合约被认为是“不良”行为,因此前导码以 INVALID (0xfe) 开头,以异常停止条件结束执行(而不是像 STOP (0x00) 这样的“更温和”的操作码)。

  • 为了帮助区分蓝图合约和其他可能以 0xFE 开头的合约,使用了“魔法”字节。值 0x71 是通过获取字节串 “blueprint” 的 keccak256 哈希的最后一个字节来任意选择的(即:keccak256(b"blueprint")[-1])。

  • 该规范不允许使用空的 initcode,以防止可能出现的常见错误。

  • 用户可能希望在其前导码中包含任意数据或代码。为了允许索引器忽略这些字节,提出了一种可变长度编码。为了允许长度只有零或一个字节(在 len(data bytes) 小于 256 的可能常见情况下),第三个字节的两位保留用于指定编码长度需要多少字节。

  • 如果我们需要升级路径,则包含版本位。虽然我们不希望耗尽版本位,但如果这样做,则保留延续序列。由于 <length bytes> 只需要两个字节(因为 EIP-170 将合约长度限制为 24KB),因此永远不需要值为 3 的 <length encoding bits> 来描述 <length bytes>。因此,特殊的 <length encoding bits>0b11 保留为延续序列标记。

  • 默认情况下,initcode 本身的长度不包含在前导码中,因为它占用空间,并且可以使用 EXTCODESIZE 轻松确定。

  • 以太坊对象格式 (EOF) 可以通过添加另一个部分类型(3 - initcode)来提供另一种指定蓝图合约的方法。但是,它尚未进入 EVM,我们希望能够在今天标准化蓝图合约,而不依赖 EVM 更改。如果在将来的某个时候,section kind 3 成为 EOF 规范的一部分,并且 EOF 成为 EVM 的一部分,则此 ERC 将被认为已过时,因为 EOF 验证规范提供了比此 ERC 更强的保证。

向后兼容性

没有已知问题

测试用例

  • 一个示例(且微不足道!)没有数据部分的蓝图合约,其 initcode 只是 STOP 指令:
0xFE710000
  • 一个示例蓝图合约,其 initcode 是微不足道的 STOP 指令,其数据部分包含重复七次的字节 0xFF
0xFE710107FFFFFFFFFFFFFF00

在此,0xFE71 是魔法头,0x01 表示版本 0 + 1 个长度位,0x07 以字节为单位编码数据部分的长度。接下来是数据部分,然后是 initcode。为了说明,带有分隔符的上述代码将是 0xFE71|01|07|FFFFFFFFFFFFFF|00

  • 一个示例蓝图,其 initcode 是微不足道的 STOP 指令,其数据部分包含重复 256 次的字节 0xFF
0xFE71020100FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00

分隔后,这将是 0xFE71|02|0100|FF...FF|00

参考实现

from typing import Optional, Tuple

def parse_blueprint_preamble(bytecode: bytes) -> Tuple[int, Optional[bytes], bytes]:
    """
    Given bytecode as a sequence of bytes, parse the blueprint preamble and
    deconstruct the bytecode into:
        the ERC version, preamble data and initcode.
    Raises an exception if the bytecode is not a valid blueprint contract
    according to this ERC.
    arguments:
        bytecode: a `bytes` object representing the bytecode
    returns:
        (版本,
         如果 <length encoding bits> 为 0,则为 None,否则为数据部分的字节,
         initcode 的字节,
        )
    """
    if bytecode[:2] != b"\xFE\x71":
        raise Exception("Not a blueprint!")

    erc_version = (bytecode[2] & 0b11111100) >> 2

    n_length_bytes = bytecode[2] & 0b11
    if n_length_bytes == 0b11:
        raise Exception("Reserved bits are set")

    data_length = int.from_bytes(bytecode[3:3 + n_length_bytes], byteorder="big")

    if n_length_bytes == 0:
        preamble_data = None
    else:
        data_start = 3 + n_length_bytes
        preamble_data = bytecode[data_start:data_start + data_length]

    initcode = bytecode[3 + n_length_bytes + data_length:]

    if len(initcode) == 0:
        raise Exception("Empty initcode!")

    return erc_version, preamble_data, initcode

以下参考函数将蓝图所需的 initcode 作为参数,并返回将部署相应蓝图合约(没有数据部分)的 EVM 代码:

def blueprint_deployer_bytecode(initcode: bytes) -> bytes:
    blueprint_preamble = b"\xFE\x71\x00"  # ERC5202 preamble
    blueprint_bytecode = blueprint_preamble + initcode

    # the length of the deployed code in bytes
    # 部署的代码的长度(以字节为单位)
    len_bytes = len(blueprint_bytecode).to_bytes(2, "big")

    # copy <blueprint_bytecode> to memory and `RETURN` it per EVM creation semantics
    # PUSH2 <len> RETURNDATASIZE DUP2 PUSH1 10 RETURNDATASIZE CODECOPY RETURN
    # 根据 EVM 创建语义将 <blueprint_bytecode> 复制到内存并 `RETURN` 它
    deploy_bytecode = b"\x61" + len_bytes + b"\x3d\x81\x60\x0a\x3d\x39\xf3"

    return deploy_bytecode + blueprint_bytecode

安全注意事项

可能已经存在链上的合约,这些合约恰好以与本 ERC 中提出的相同的前缀开头。但是,这不被认为是严重的风险,因为索引器使用它的方式是通过编译源代码并在前面加上前导码来验证源代码。

截至 2022-07-08,部署在以太坊主网上的合约都没有以 0xFE71 开头的字节码。

版权

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

Citation

Please cite this document as:

Charles Cooper (@charles-cooper), Edward Amor (@skellet0r), "ERC-5202: 蓝图合约格式," Ethereum Improvement Proposals, no. 5202, June 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5202.