当 Solidity 为要部署的智能合约生成 bytecode 时,它会在 bytecode 的末尾附加有关编译的元数据。我们将检查此 bytecode 中包含的数据。
一个简单的智能合约
让我们来看看最简单的 Solidity 智能合约的编译器输出
//SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract Empty {
constructor() payable {}
}
这个合约实际上什么也不做。我们可以使用 solc --optimize-runs 1000 --bin C.sol 对其进行编译以查看 init code。我们会得到以下输出
======= C.sol:Empty =======
Binary:
6080604052603e80600f5f395ff3fe60806040525f80fdfea26469706673582212203082dbb4f4db7e5d53b235f44d3e38f839dc82075e2cda9df05b88e6585bca8164736f6c63430008140033
对于一个什么都不做的合约来说,这看起来太大了,对吧?让我们来了解一下这些 bytecode 到底是什么。
当我们使用 solc --optimize-runs 1000 --bin --no-cbor-metadata C.sol 编译代码时,得到以下输出:
======= C.sol:Empty =======
Binary:
6080604052600880600f5f395ff3fe60806040525f80fd
这小得多了!那么所有这些额外的信息是什么呢?
Solidity 元数据
默认情况下,Solidity 编译器会在“实际”的 init code 末尾附加元数据,当 constructor 执行完成时,这些代码会被存储到区块链上。以下是这些“额外”的代码:
fea26469706673582212203082dbb4f4db7e5d53b235f44d3e38f839dc82075e2cda9df05b88e6585bca8164736f6c63430008140033
最后两个字节 0033 的意思是“向后看 0x33 个字节,那就是元数据”。这指的是前面 fe(即 INVALID opcode)和结尾 0033 之间的所有代码。我们可以检查这确实是 0x33 个字节。
# fe and 0033 are not included
>>>hex(len('a26469706673582212203082dbb4f4db7e5d53b235f44d3e38f839dc82075e2cda9df05b88e6585bca8164736f6c6343000814') // 2)
# '0x33'
那么这 0x33(十进制为 51)长度的字符串是什么呢?
如果我们对源代码做一个微小的、看似无关紧要的更改,我们就能得到一些提示。这个更改实际上只是增加了一条注释。
//SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract Empty {
// nothing
constructor() payable {}
}
以下是修改前后的截图对比。

你可以看到,即使代码的功能没有改变,带有下划线的部分也发生了改变。我们将在下一节中解释方框中的代码。
解码元数据
起初,这看起来就像我们在凭空施放魔法来提取子字符串;请耐心听我们解释。
让我们看看上方蓝色框中的十六进制
>>> bytes.fromhex("69706673").decode("ASCII")
'ipfs'
接下来让我们看看红色框中的代码
>>> bytes.fromhex("736f6c63").decode("ASCII")
'solc'
这为我们提供了有关该数据包含什么内容的线索:一个 IPFS 哈希和 Solidity 编译器版本。
IPFS 哈希
带有黄色下划线的部分与青绿色框的内容,可以被放入以下 python 脚本中(请注意,我们使用的是带有 // nothing 注释的代码版本)
import base58
hex_ipfs_hash = "12206a68b6b8bcc01ba559ec3adf7a387b6c4210a5dc69a05d038e9d17cae3fa373b"
bytes_str = bytes.fromhex(hex_ipfs_hash)
print(base58.b58encode(bytes_str).decode("utf-8"))
# QmVW2XyafSxDtiSqirJRauuT5SaQtGnQYsxxyYHrFmRTEa
这个 Qm...RTEa 是编译器生成的元数据文件的 IPFS 哈希。这部分代码(青绿色和黄色)的编码方式与上面的方框不同。具体来说,IPFS 哈希(青绿色和黄色部分)是十六进制数据 “1220…RTEa” 的 base58 编码版本。
如果你将 Solidity 编译器的 JSON 文件放到 IPFS 上,你就会得到这个 IPFS 哈希。下面就是提到的这个 JSON 文件。

我们可以将该 JSON 文件保存为一个实际的文件,然后验证其哈希是否与我们上面在 python 中生成的哈希匹配。你需要安装 ipfs 命令行工具(如何安装)。
mkdir out
solc --optimize-runs 1000 --bin --metadata C.sol --output-dir out
# Compiler run successful. Artifact(s) can be found in directory "out".
ipfs add -qr --only-hash out/Empty_meta.json
# QmVW2XyafSxDtiSqirJRauuT5SaQtGnQYsxxyYHrFmRTEa
这与早先获取的哈希相匹配。
这不会导致哈希冲突吗?
如果两个具有相同源代码和编译器配置的合约将其经过验证的源代码存储在 IPFS 上,IPFS 哈希将会发生冲突,但这实际上是我们所希望的,因为它节省了存储空间。智能合约是由 chain id 及其 address 的组合来唯一标识的,而不是由 IPFS 内容标识的。
获取 Solidity 版本
最后,如果我们转换橙色框中的部分,我们就会看到 Solidity 版本。
>>> 0x00 # solidity is version 0
0
>>> 0x08 # major version
8
>>> 0x14 # minor version
20
# correct, we used solidity 0.8.20
为什么存在智能合约元数据?
这种元数据为部署成本增加了额外的 53 个字节,这转化为额外的 10,600 gas(每个 bytecode 200 gas)+ calldata 成本(非零字节每个 16 gas,零字节每个 4 gas)。这转化为 calldata 成本中高达 848 的额外 gas。
那么为什么要包含它呢?
这使得智能合约代码能够被严格验证。编译器输出的元数据 JSON 包含了源代码的哈希。因此,如果源代码发生了微小的改变,元数据 JSON 文件也会改变,其 IPFS 哈希也会随之改变。
通过 IPFS 哈希降低 gas 的奇妙技巧
在部署时优化 gas 成本的一个明显方法是使用 --no-cbor-metadata 选项。但是,如果你需要将其用于合约验证,你仍然可以通过挖掘包含大量零字节的 IPFS 哈希来降低 gas 成本。在部署合约时,零字节将降低 calldata 的成本。因为源代码(包含注释在内)是被哈希处理过的,这意味着我们可以通过挖掘特定的注释,来产生 gas 效率更高的 IPFS 哈希,并将其附加到合约中。请注意,这意味着我们希望该哈希的十六进制表示形式包含零,而不是 base58 编码。
进一步阅读资源
你可以在相关的 Solidity 文档中查看用于操作此元数据的所有选项。Sourcify 提供了一个解析现有智能合约元数据的工具。
了解更多
请参阅我们的 Solidity Bootcamp 以学习更高级的智能合约主题。
最初发布于 2023 年 5 月 25 日