合约元数据

Solidity 编译器会自动生成一个 JSON 文件。该文件包含有关已编译合约的两种信息

  • 如何与合约交互:ABI 和 NatSpec 文档。

  • 如何重现编译并验证已部署的合约:编译器版本、编译器设置和使用的源文件。

默认情况下,编译器会将元数据文件的 IPFS 哈希追加到每个合约的运行时字节码(不一定是创建字节码)的末尾,以便在发布时,您可以以经过身份验证的方式检索文件,而无需依赖于集中式数据提供者。其他可用选项是 Swarm 哈希和不将元数据哈希追加到字节码。这些可以通过 标准 JSON 接口 配置。

您必须将元数据文件发布到 IPFS、Swarm 或其他服务,以便其他人可以访问它。您可以使用 solc --metadata 命令以及 --output-dir 参数来创建文件。如果没有该参数,元数据将被写入标准输出。元数据包含指向源代码的 IPFS 和 Swarm 引用,因此除了元数据文件之外,您还必须上传所有源文件。对于 IPFS,ipfs add 返回的 CID 中包含的哈希(不是文件的直接 sha2-256 哈希)应与字节码中包含的哈希匹配。

元数据文件具有以下格式。下面的示例以人类可读的方式呈现。格式正确的元数据应正确使用引号,将空格减少到最少,并将所有对象的键按字母顺序排序,以达到规范的格式。注释是不允许的,这里仅用于说明目的。

{
  // Required: Details about the compiler, contents are specific
  // to the language.
  "compiler": {
    // Optional: Hash of the compiler binary which produced this output
    "keccak256": "0x123...",
    // Required for Solidity: Version of the compiler
    "version": "0.8.2+commit.661d1103"
  },
  // Required: Source code language, basically selects a "sub-version"
  // of the specification
  "language": "Solidity",
  // Required: Generated information about the contract.
  "output": {
    // Required: ABI definition of the contract. See "Contract ABI Specification"
    "abi": [/* ... */],
    // Required: NatSpec developer documentation of the contract. See https://docs.soliditylang.cn/en/latest/natspec-format.html for details.
    "devdoc": {
      // Contents of the @author NatSpec field of the contract
      "author": "John Doe",
      // Contents of the @dev NatSpec field of the contract
      "details": "Interface of the ERC20 standard as defined in the EIP. See https://eips.ethereum.org/EIPS/eip-20 for details",
      "errors": {
        "MintToZeroAddress()" : {
          "details": "Cannot mint to zero address"
        }
      },
      "events": {
        "Transfer(address,address,uint256)": {
          "details": "Emitted when `value` tokens are moved from one account (`from`) toanother (`to`).",
          "params": {
            "from": "The sender address",
            "to": "The receiver address",
            "value": "The token amount"
          }
        }
      },
      "kind": "dev",
      "methods": {
        "transfer(address,uint256)": {
          // Contents of the @dev NatSpec field of the method
          "details": "Returns a boolean value indicating whether the operation succeeded. Must be called by the token holder address",
          // Contents of the @param NatSpec fields of the method
          "params": {
            "_value": "The amount tokens to be transferred",
            "_to": "The receiver address"
          },
          // Contents of the @return NatSpec field.
          "returns": {
            // Return var name (here "success") if exists. "_0" as key if return var is unnamed
            "success": "a boolean value indicating whether the operation succeeded"
          }
        }
      },
      "stateVariables": {
        "owner": {
          // Contents of the @dev NatSpec field of the state variable
          "details": "Must be set during contract creation. Can then only be changed by the owner"
        }
      },
      // Contents of the @title NatSpec field of the contract
      "title": "MyERC20: an example ERC20",
      "version": 1 // NatSpec version
    },
    // Required: NatSpec user documentation of the contract. See "NatSpec Format"
    "userdoc": {
      "errors": {
        "ApprovalCallerNotOwnerNorApproved()": [
          {
            "notice": "The caller must own the token or be an approved operator."
          }
        ]
      },
      "events": {
        "Transfer(address,address,uint256)": {
          "notice": "`_value` tokens have been moved from `from` to `to`"
        }
      },
      "kind": "user",
      "methods": {
        "transfer(address,uint256)": {
          "notice": "Transfers `_value` tokens to address `_to`"
        }
      },
      "version": 1 // NatSpec version
    }
  },
  // Required: Compiler settings. Reflects the settings in the JSON input during compilation.
  // Check the documentation of standard JSON input's "settings" field
  "settings": {
    // Required for Solidity: File path and the name of the contract or library this
    // metadata is created for.
    "compilationTarget": {
      "myDirectory/myFile.sol": "MyContract"
    },
    // Required for Solidity.
    "evmVersion": "london",
    // Required for Solidity: Addresses for libraries used.
    "libraries": {
      "MyLib": "0x123123..."
    },
    "metadata": {
      // Reflects the setting used in the input json, defaults to "true"
      "appendCBOR": true,
      // Reflects the setting used in the input json, defaults to "ipfs"
      "bytecodeHash": "ipfs",
      // Reflects the setting used in the input json, defaults to "false"
      "useLiteralContent": true
    },
    // Optional: Optimizer settings. The fields "enabled" and "runs" are deprecated
    // and are only given for backward-compatibility.
    "optimizer": {
      "details": {
        "constantOptimizer": false,
        "cse": false,
        "deduplicate": false,
        // inliner defaults to "false"
        "inliner": false,
        // jumpdestRemover defaults to "true"
        "jumpdestRemover": true,
        "orderLiterals": false,
        // peephole defaults to "true"
        "peephole": true,
        "yul": true,
        // Optional: Only present if "yul" is "true"
        "yulDetails": {
          "optimizerSteps": "dhfoDgvulfnTUtnIf...",
          "stackAllocation": false
        }
      },
      "enabled": true,
      "runs": 500
    },
    // Required for Solidity: Sorted list of import remappings.
    "remappings": [ ":g=/dir" ]
  },
  // Required: Compilation source files/source units, keys are file paths
  "sources": {
    "settable": {
      // Required (unless "url" is used): literal contents of the source file
      "content": "contract settable is owned { uint256 private x = 0; function set(uint256 _x) public { if (msg.sender == owner) x = _x; } }",
      // Required: keccak256 hash of the source file
      "keccak256": "0x234..."
    },
    "myDirectory/myFile.sol": {
      // Required: keccak256 hash of the source file
      "keccak256": "0x123...",
      // Optional: SPDX license identifier as given in the source file
      "license": "MIT",
      // Required (unless "content" is used, see above): Sorted URL(s)
      // to the source file, protocol is more or less arbitrary, but an
      // IPFS URL is recommended
      "urls": [ "bzz-raw://7d7a...", "dweb:/ipfs/QmN..." ]
    }
  },
  // Required: The version of the metadata format
  "version": 1
}

警告

由于结果合约的字节码默认包含元数据哈希,因此对元数据的任何更改都可能导致字节码的更改。这包括对文件名或路径的更改,并且由于元数据包含所有使用的源文件的哈希,因此单个空格更改会导致不同的元数据和不同的字节码。

注意

上面的 ABI 定义没有固定的顺序。它可能会随着编译器版本的改变而改变。不过,从 Solidity 版本 0.5.12 开始,该数组保持一定的顺序。

元数据哈希在字节码中的编码

编译器目前默认将 IPFS 哈希(在 CID v0 中)、规范元数据文件和编译器版本追加到字节码的末尾。可以选择使用 Swarm 哈希而不是 IPFS,或者使用实验性标志。以下是所有可能的字段

{
  "ipfs": "<metadata hash>",
  // If "bytecodeHash" was "bzzr1" in compiler settings not "ipfs" but "bzzr1"
  "bzzr1": "<metadata hash>",
  // Previous versions were using "bzzr0" instead of "bzzr1"
  "bzzr0": "<metadata hash>",
  // If any experimental features that affect code generation are used
  "experimental": true,
  "solc": "<compiler version>"
}

由于我们将来可能支持其他检索元数据文件的方法,因此此信息以 CBOR 编码存储。字节码中的最后两个字节表示 CBOR 编码信息的长度。通过查看此长度,可以使用 CBOR 解码器解码字节码的相关部分。

查看 元数据游乐场 了解其在实际中的应用。

虽然 solc 的发行版使用上面所示的版本的三字节编码(每个字节分别代表主版本号、次版本号和补丁版本号),但预发行版将使用包含提交哈希和构建日期的完整版本字符串。

命令行标志 --no-cbor-metadata 可用于跳过元数据从已部署字节码末尾追加。同样,标准 JSON 输入中的布尔字段 settings.metadata.appendCBOR 可以设置为 false。

注意

CBOR 映射也可以包含其他键,因此最好通过查看字节码末尾的 CBOR 长度来完全解码数据,并使用适当的 CBOR 解析器。不要依赖于它从 0xa2640xa2 0x64 'i' 'p' 'f' 's' 开始。

用于自动接口生成和 NatSpec

元数据以以下方式使用:希望与合约交互的组件(例如钱包)检索合约的代码。它解码包含元数据文件的 IPFS/Swarm 哈希的 CBOR 编码部分。使用该哈希,检索元数据文件。该文件被 JSON 解码成上面的结构。

然后,组件可以使用 ABI 自动为合约生成一个基本的用户界面。

此外,钱包可以使用 NatSpec 用户文档,在每次用户与合约交互时,向用户显示一条人类可读的确认消息,并同时请求交易签名的授权。

有关更多信息,请阅读 以太坊自然语言规范 (NatSpec) 格式

用于源代码验证

如果已固定/发布,则可以从 IPFS/Swarm 检索合约的元数据。元数据文件还包含源文件的 URL 或 IPFS 哈希,以及编译设置,即重现编译所需的一切。

有了这些信息,就可以通过重现编译并将编译生成的字节码与已部署合约的字节码进行比较,来验证合约的源代码。

这会自动验证元数据,因为它的哈希是字节码的一部分,以及源代码,因为它们的哈希是元数据的一部分。文件或设置的任何更改会导致不同的元数据哈希。这里的元数据充当整个编译的指纹。

Sourcify 利用此功能进行“完全/完美验证”,以及在 IPFS 上公开固定文件,以便使用元数据哈希访问。