🦁11 合约元数据和ABI编码

[TOC]

合约元数据和ABI编码

合约元数据

Solidity 编译器自动生成 JSON 文件,即合约的元数据,其中包含了当前合约的相关信息。 它可以用于查询编译器版本,所使用的源代码,ABI 和 natspec 文档,以便更安全地与合约进行交互并验证其源代码。

  • 编译器会将元数据文件的 Swarm 哈希值附加到每个合约的字节码末尾,以便可以以认证的方式获取该文件,而不必求助于中心化的数据提供者。

  • 必须将元数据文件发布到 Swarm (或其他服务),以便其他人可以访问它。 该文件可以通过使用 solc --metadata 来生成,并被命名为 ContractName_meta.json 。 它将包含源代码的在 Swarm 上的引用,因此必须上传所有源文件和元数据文件。

  • 正确格式化的元数据应正确使用引号,将空白减少到最小,并对所有对象的键值进行排序以得到唯一的格式。

元数据 JSON 文件主要字段

由于生成的合约的字节码包含元数据的哈希值,因此对元数据的任何更改都会导致字节码的更改。 此外,由于元数据包含所有使用的源代码的哈希值,所以任何源代码中的任何变化都将导致不同的元数据,并随后产生不同的字节代码。

字段
必选
示例值
描述

version

1

元数据格式的版本

language

Solidity

源代码的编程语言,一般会选择规范的“子版本”

compiler

{version,keccak256}

编译器的细节,内容视语言而定。

compiler.version

0.8.0+commit

Solidity 编译器的版本

compiler.keccak256

"0x123..."

输出的编译器二进制文件的哈希值

sources

{}

编译的源文件/源单位,键值为文件名

sources["demo.sol"]

{}

["demo.sol"].keccak256

"0x123..."

源文件的 keccak256 哈希值

["demo.sol"].urls

[ "bzzr://56ab..." ]

urls和content任选一, 已排序的源文件的URL,URL的协议可以是任意的,但建议使用 Swarm 的URL

["demo.sol"].license

MIT

在源文件中定义的 SPDX license 标识

sources["mortal"]

{}

["mortal"].keccak256

"0x234..."

源文件的 keccak256 哈希值

settings

{}

编译器的设置

settings.remappings

[ ":g/dir" ]

Solidity 必选,已排序的重定向列表

settings.optimizer

{}

优化器的设置( enabled 默认设为 false )

metadata

{}

反映在输入json中使用的设置

libraries

{}

所使用的库的地址

output

{}

合约的生成信息

output.abi

[ ... ]

合约的 ABI 定义

output.userdoc

[ ... ]

合约的 NatSpec 用户文档

output.devdoc

[ ... ]

合约的 NatSpec 开发者文档

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

在将来可能会支持其他方式来获取元数据文件, 类似 {"bzzr0":} 的键值对,将会以 CBOR 编码来存储。

  • 由于这种编码的起始位不容易找到,因此添加两个字节来表述其长度,以大端方式编码。 所以,当前版本的 Solidity 编译器,将部署的字节码的末尾添加内容:0xa1 0x65 'b' 'z' 'z' 'r' '0' 0x58 0x20 <32 bytes swarm hash> 0x00 0x29, 因此,为了获取数据,可以检查部署的字节码的末尾以匹配该模式,并使用 Swarm 哈希来获取元数据文件。

自动化接口生成

元数据以下列方式被使用:

  • 想要与合约交互的组件(例如,Mist)读取合约的字节码, 从中获取元数据文件的 Swarm 哈希,然后从 Swarm 获取该文件。该文件被解码为上面的 JSON 结构。 然后该组件可以使用ABI自动生成合约的基本用户接口。

  • 此外,Mist可以使用 userdoc 在用户与合约进行交互时向用户显示确认消息。

源代码验证的使用方法

为了验证编译,可以通过元数据文件中的链接从 Swarm 中获取源代码。 获取到的源码,会根据元数据中指定的设置,被正确版本的编译器(“官方”编译器之一)所处理。 处理得到的字节码会与创建交易的数据或者 CREATE 操作码使用的数据进行比较。这会自动验证元数据,因为它的哈希值是字节码的一部分。 而额外的数据,则是与基于接口进行编码并展示给用户的构造输入数据相符的。

ABI 编码

在以太坊生态系统中,应用二进制接口 Application Binary Interface(ABI) 是从区块链外部与合约进行交互以及合约与合约间进行交互的一种标准方式。 数据会根据其类型按照这份手册中说明的方法进行编码。这种编码并不是可以自描述的,而是需要一种特定的概要(schema)来进行解码。

  • 从外部施加给以太坊的行为都称之为向以太坊网络提交了一个交易, 调用合约函数是向合约地址(账户)提交了一个交易,这个交易有一个附加数据,这个附加的数据就是ABI的编码数据。因此要想和合约交互,就离不开ABI数据。

  • ABI 编码主要用途就是远程调用合约,用合约函数名ABI编码+参数编码做成一串ABI编码的DATA来达到调用合约的目的,这在设计代理模式可升级合约中很常用。

类型编码

基础类型编码:

  • uint:M 位的无符号整数,0 < M <= 256、M % 8 == 0。例如:uint32,uint8,uint256。

  • int:以 2 的补码作为符号的 M 位整数,0 < M <= 256、M % 8 == 0。

  • address:除了字面上的意思和语言类型的区别以外,等价于 uint160。在计算和 函数选择器Function Selector 中,通常使用 address。

  • uint、int:uint256、int256 各自的同义词。在计算和 函数选择器Function Selector 中,通常使用 uint256 和 int256。

  • bool:等价于 uint8,取值限定为 0 或 1 。在计算和 函数选择器Function Selector 中,通常使用 bool。

  • fixedx:M 位的有符号的固定小数位的十进制数字 8 <= M <= 256、M % 8 == 0、且 0 < N <= 80。其值 v 即是 v / (10 ** N)。(也就是说,这种类型是由 M 位的二进制数据所保存的,有 N 位小数的十进制数值。译者注。)

  • ufixedx:无符号的 fixedx。

  • fixed、ufixed:fixed128x18、ufixed128x18 各自的同义词。在计算和 函数选择器Function Selector 中,通常使用 fixed128x18 和 ufixed128x18。

  • bytes:M 字节的二进制类型,0 < M <= 32。

  • function:一个地址(20 字节)之后紧跟一个 函数选择器Function Selector (4 字节)。编码之后等价于 bytes24。

定长数组类型:

  • [M]:有 M 个元素的定长数组,M >= 0,数组元素为给定类型。

非定长类型:

  • bytes:动态大小的字节序列。

  • string:动态大小的 unicode 字符串,通常呈现为 UTF-8 编码。

  • []:元素为给定类型的变长数组。

元组tuple:

  • 可以将若干类型放到一对括号中,用逗号分隔开,以此来构成一个 元组tuple。

  • (T1,T2,...,Tn):由 T1,…,Tn,n >= 0 构成的 元组tuple。

  • 用元组tuple 构成元组tuple、用元组tuple 构成数组等等也可以。另外也可以构成“零元组(zero-tuples)”,就是 n = 0 的情况。

Solidty 对应 ABI 类型

  • 除了元组之外,Solidity 支持上面提到的所有具有相同名称的类型。另一方面,ABI 不支持某些 Solidity 类型。

Solidity
ABI

address payable

address

contract

address

enum

uint8

struct

tuple

编码的设计准则

编码被设计为具有以下两个属性,如果某些参数是嵌套数组,这些属性特别有用:

  • 1: 访问一个值所需的读取次数最多是参数数组结构中值的深度,即需要四次读取来检索 a_i[k][l][r]。在 ABI 的早期版本中,在最坏的情况下,读取次数与动态参数的总数呈线性关系。

  • 2: 变量或数组元素的数据不与其他数据交错,它是可重定位的,即它只使用相对“地址”。

编码的形式化说明

需要区分静态和动态类型。静态类型会被直接编码,动态类型则会在当前数据块之后单独分配的位置被编码。

动态类型:

  • bytes

  • string

  • 任意类型 T 的变长数组 T[]

  • 任意动态类型 T 的定长数组 T[k] (k >= 0)

  • 由动态的 Ti (1 <= i <= k)构成的 元组tuple (T1,...,Tk);

  • 其他类型都是静态类型。

函数选择器和参数编码

函数选择器 Function Selector

  • 一个函数调用数据的前 4 字节,指定了要调用的函数。这就是某个函数签名的 Keccak 哈希的前 4 字节(高位在左的大端序,指最高位字节存储在最低位地址上的一种串行化编码方式,即高位字节在左。 这种签名被定义为基础原型的规范表达,基础原型即是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。

  • 函数的返回类型不是这个签名的一部分。在 Solidity 的函数重载 中,返回值并没有被考虑。这是为了使对函数调用的解析保持上下文无关。 然而 应用二进制接口 Application Binary Interface(ABI) 的 JSON 描述中包含了即包含了输入也包含了输出。

合约 demo2 例子:

  • demo2 有两个函数 setDatadata状态变量的getter函数。

  • 调用 setData(88) 支付 gas,查看交易详情数据 input 字段的数据:0x5b4b73a90000000000000000000000000000000000000000000000000000000000000058,这是ABI编码数据,分为两部分 0x5b4b73a9 是函数选择器公4字节,而后面的00...58 是数据 88(88的十六进制就是58)。

计算参数ABI编码

  • Solidity 提供了ABI的相关API,用来计算ABI编码信息:

  • 计算ABI编码:

非标准打包模式

Solidity 支持一种非标准打包模式:

  • 函数选择器 不进行编码,

  • 长度低于 32 字节的类型,既不会进行补 0 操作,也不会进行符号扩展,以及 动态类型会直接进行编码,并且不包含长度信息。

  • 例如,对 int1, bytes1, uint16, string 用数值 -1, 0x42, 0x2424, "Hello, world!" 进行编码将生成如下结果

  • 更具体地说,每个静态大小的类型都尽可能多地按它们的数值范围使用了字节数,而动态大小的类型,像 string、 bytes 或 uint[],在编码时没有包含其长度信息。 这意味着一旦有两个动态长度的元素,编码就会变得有歧义了。

Last updated