Solidity学习 - ABI 应用二进制接口
文章目录
- 一、ABI 基础概念
- 1. ABI 与 API 的区别
- 2. ABI 的核心作用
- 二、ABI 接口描述
- 1. 编译后的产物
- 2. ABI JSON 格式示例
- 3. ABI JSON 关键字段说明
- 三、ABI 编码
- 1. 编码示例
- 2. 编码数据的组成
- 3. Solidity 中的编码函数
- 四、ABI 解码
- 1. 解码的基本概念
- 2. 事件日志的解码
- 五、ABI 编解码可视化工具
一、ABI 基础概念
1. ABI 与 API 的区别
- API(应用程序接口):是两个软件之间进行通信的桥梁,用于访问某个服务。
- ABI(应用二进制接口):定义了智能合约中可交互的方法、事件和错误,是与 EVM(以太坊虚拟机)交互的桥梁。
2. ABI 的核心作用
EVM 只能识别和运行由 0 和 1 组成的二进制数据,因此在调用函数时,需要借助 ABI 将人类可读的函数转化为 EVM 可读的字节码。从本质上说,ABI 是编码和解码规范,用于规范外部与 EVM 的交互,也可用于合约间的交互。
二、ABI 接口描述
1. 编译后的产物
在 Solidity 中编译代码后,会得到两个重要的 artifact
(产物):
- bytecode(字节码):合约部署到区块链上的实际代码。
- ABI 接口描述:是 JSON 格式的文件,定义了智能合约中外部可交互的方法、事件和可解释的错误。
2. ABI JSON 格式示例
以 Counter 合约为例,其编译后生成的 ABI 是一个 JSON 格式的数组,每个对象定义了合约中可公开调用的方法(函数)、声明的事件及错误等。以下是 Counter 合约及其对应的 ABI 示例:
contract Counter {uint public counter;address private owner; error NotOwner(); event Set(uint _value); constructor() {owner = msg.sender;} function set(uint x) public { if(owner != msg.sender) revert NotOwner(); counter = x;emit Set(x); }
}
对应的 ABI 如下:
[{"inputs": [], "name": "NotOwner", "type": "error"},{"anonymous": false, "inputs": [{ "indexed": false, "internalType": "uint256", "name": "_value", "type": "uint256"}],"name": "Set", "type": "event"},{"inputs": [], "name": "counter", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},{"inputs": [{ "internalType": "uint256", "name": "x", "type": "uint256"}],"name": "set", "outputs": [], "stateMutability": "nonpayable", "type": "function"}
]
3. ABI JSON 关键字段说明
- type:定义是函数、事件或错误等。
- name:表示函数名称、事件名称、自定义错误名称。
- inputs:函数输入参数。
- outputs:函数输出参数。
三、ABI 编码
1. 编码示例
以调用部署在 sepolia 网络上的 Counter 合约的 set()
函数,并传入参数 10 为例,经过 ABI 编码后提交到链上的数据是 0x60fe47b1000000000000000000000000000000000000000000000000000000000000000a
。
2. 编码数据的组成
该编码数据包含两个部分:
- 函数选择器(前 4 个字节):
0x60fe47b1
,它是 ABI 描述中函数的签名set(uint256)
进行 keccak256 哈希运算后取前 4 个字节,即bytes4(keccak256("set(uint256)")) == 0x60fe47b1
。 - 参数编码:参数 10 的十六进制是
a
,然后扩展到 32 个字节。
3. Solidity 中的编码函数
Solidity 中有 5 个用于编码的函数:
- abi.encode:按 EVM 标准规则对参数编码,每个参数按 32 个字节填充 0 后再拼在一起,用于与合约交互时编码参数。
- abi.encodePacked:紧密编码,参数编码拼接时不填充 0,使用实际占用空间拼接,若结果不是 32 字节整数倍数,在末尾填充 0,例如在使用 EIP712 时需要用到。
- abi.encodeWithSignature:对函数签名及参数进行编码,第一个参数是函数签名,后面按 EVM 标准规则对参数编码,可直接获得调用函数所需的 ABI 编码数据。
- abi.encodeWithSelector:与
abi.encodeWithSignature
功能类似,第一个参数为 4 个字节的函数选择器。 - abi.encodeCall:通过函数指针对函数及参数编码,执行编码时进行完整的类型检查,确保类型匹配函数签名。
四、ABI 解码
1. 解码的基本概念
解码是编码的“逆过程”。区块链浏览器能将提交给链上的 0x60fe47b10000000...0a
显示为函数 set(uint256 x)
,就是对数据进行了解码。需要注意的是,仅能对参数进行解码,函数选择器因使用了 keccak256 哈希运算(哈希不可逆)无法直接解码,但开源合约代码后,区块链浏览器可计算出所有函数的函数选择器,从而通过函数选择器匹配对应的函数签名。
2. 事件日志的解码
ABI 解码的一个重要场景是解析交易中的事件日志。日志包含 Topics 和 Data 两部分,其中 Topics 的第一个主题是事件签名的 Keccak256 哈希。通过匹配该哈希值,可知道 EVM 产生的日志由哪个事件生成,进而根据事件的参数列表解析日志数据。Solidity、web3.js、ethers.js 库都提供了解码函数。
五、ABI 编解码可视化工具
ChainToolDAO 开发了几个可视化工具,可帮助进行编解码:
- 函数选择器的查询及反查:https://chaintool.tech/querySelector
- 事件签名的 Topic 查询:https://chaintool.tech/topicID
- Hash 工具(提供 Keccak-256 及 Base64):https://chaintool.tech/hashTool
- 交易数据(calldata)的编码与解码:https://chaintool.tech/calldata