你的位置:首页 > 公告动态

什么是 Input Data?在跟智能合约交互(例如发送 token)时

2021/6/3 11:06:32      点击:
我们先来看看这笔 token 转账交易。某个人发送了 0 ETH 到 0xd26114cd6ee289accf82350c8d8487fedb8a0c07(OmiseGo 合约地址),而且 Etherscan 网站呈现了这是一笔意图发送 0.19 OMG token 到这个地址的事务。那么,EVM (以太坊虚拟机)究竟是怎么知道,这个人想要转账某个数额的 token 到另一地址的呢?


你再仔细看 Etherscan,就能看到这笔事务带着 input data。input data 是发送者为这笔事务附加的额外数据,既可以是普通的文本,也可以是数字(以十六进制的形式编码)。但在这笔交易中,发送者使用这部分数据来 “告诉” 合约,让合约运行特定的函数。智能合约本身是由一系列函数组成的。举例而言,一个 ERC-20 token 合约使用比如 “transfer” 来把 token 从 A 账户转移到 B账户,使用 “balancerOf” 函数来获得某个地址的余额,等等。在我们研究的这笔交易中,你可以看到它调用了 transfer(address_to, uint256_value) 函数。


这笔事务的输入数据为0xa9059cbb0000000000000000000000004bbeeb066ed09b7aed07bf39eee0460dfa26152000000000000000000000000000000000000000000000000002a34892d36d6c74。你可以把这一长串的 十六进制 数据分解一下。开头的 0x 表示这是一个十六进制数值,紧接着的 8 个字节(a9059cbb)是函数标识符,再然后就全部是以 32 字节(也就是 64 个 16 进制字符)为一组的函数参数。 所以第一组是 0000000000000000000000004bbeeb066ed09b7aed07bf39eee0460dfa261520 而第二组是 000000000000000000000000000000000000000000000000002a34892d36d6c74。






- Input Data 分解 -
如果你在 Etherscan 上查看这些数据,你会看到它以下文这个形式呈现:


Function: transfer(address _to, uint256 _value)
MethodID: 0xa9059cbb
[0]: 0000000000000000000000004bbeeb066ed09b7aed07bf39eee0460dfa261520
[1]: 00000000000000000000000000000000000000000000000002a34892d36d6c74
十六进制是啥?


十六进制是一种计数系统,就像十进制和二进制一样;十六进制使用数字 0 到 9 和字母 A 到 F(不区分大小写),来对应表示十进制的 0 到 15。下面这种图展现的就是这样的对应关系。十六进制常常用来更直观地表示大数字。






- 十进制数字与对应的十六进制字符 -
单个十六进制字符所能表示的最大数值是 15,长度是 4 个比特(bit)。多个十六进制字符相连时,你要把每个字符的二进制表示前后拼接在一起,才能得到其十进制数值。举个例子,0x5C,可以写成 0101 (=5) 和 1100 (=C),前后拼接就是 01011100,这就是二进制形式的 92,所以十六进制数 0x5C 的数值就是 92。


大多数编程语言都使用前缀 0x 作为绝对标识符(arbitrary identifier),将十六进制数与其他的计数类型(比如普通的十进制、二进制等)区别开来。这个前缀本身没有任何意义,只是为了清晰。我们这篇文章也会采取一样的做法,十六进制数都用 0x 开头。


讲完这些,我们继续。如果你还是没能理解十六进制,也不用担心 —— 对于理解 input data 来说不是必需的。


Input Data 与智能合约


Input Data 的首要用途就是与智能合约交互。大部分智能合约都使用 合约 ABI 规范,使得 Etherscan 这样的网站能自动解码 input data 并显示事务所调用的具体操作。在我们上面那个例子中,这是一笔有关代币合约的事务,而且代币合约遵循 ERC-20 标准。这也就意味着,我们都知晓所有可能调用的函数,以及它们的 签名。举例,用于 ERC-20 合约的 transfer(转账)函数的完整签名总是 transfer(address, uint256),意味着这个函数需要两个参数,所传入的第一个参数会被解读为一个地址,第二个参数会被解读为一个未签名的 256 位的数字(大小上限为 2256-1)。


Solidity 语言有多种参数类型。如果你有兴趣学习 Solidity 语言和智能合约,你可以在Solidity 文档页面了解更多。


函数签名


如你所见,transfer 函数的签名是 transfer(address, uint256),这个对所有 ERC-20 合约都是一样的。如果某个合约给转账函数安排不一样的参数类型,比如一个地址和一个 uint128(未签名的 128 位整数),这个合约就不是 “ERC-20 兼容” 的。


要获得一个函数的签名的十六进制形式,我们先要获得这个函数的 SHA-3(或者说 Keccak-256)哈希值的前面 4 个字节(也就是 8 个十六进制字符)。而要想知道一个数据的 Keccak-256 哈希值,你可以使用 JavaSceript 语言的 web3 库,或者求助于这样的在线工具。在这个工具页面填入 transfer(address,uint256),它会显示 0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b 作为结果。取前 8 个字符(忽略掉 0x),就是 a9059cbb,恰好跟上述事务的 MethodID 一致。


另一个例子:ERC-20 标准合约的 approve(许可)函数的函数签名是 approve(address,uint256),其 SHA-3 哈希值是 0x095ea7b334ae44009aa867bfb386f5c3b4b443ac6f0ee573fa91c4608fbadfba,首 8 个字符是 095ea7b3,因此,调用许可函数的 input data 开头就会是 0x095ea7b3。这笔发往 DAI token 合约的事务就是如此。


地址和数量


每一个参数(除了 列表/数组 和纯文本 —— 这些我们后文再说)的长度都是 32 字节,或者说 64 个十六进制字符。但以太坊地址只有 40 个字节长(不算 0x 的话)。为了解决这个问题,地址参数要用 0 来填充。在十六进制里面,0x0000123 和 0x123 是一样的,因此 0x0000000000000000000000004bbeeb066ed09b7aed07bf39eee0460dfa261520(上述事务中的地址参数)等同于 0x4bbeeb066ed09b7aed07bf39eee0460dfa261520,而且 0x00000000000000000000000000000000000000000000000002a34892d36d6c74 也就等于 0x2a34892d36d6c74。那为什么我们要填充这些 0 呢?


就像我们上面说到的,Solidity 合约可以接受的最大数值是 2256 - 1,刚好是 32 字节。使用固定的长度可以让 EVM 和其他应用在解码数据时候更轻松,因为你可以假设每一个参数的长度都是一样的。


那数组和字符串呢?


如上所述,在 input data 中使用数组和字符串,情形会有些许不同。因为数组本质是多个东西组成的一个列表。举个例子,1、2、3 三个数所组成的列表在大多数编程语言中都可以写为 [1, 2, 3]。要在事务中发送这种数据,列表中的每一个对象都要作为 32 字节一组的数据发送,列在 input data 的结尾。指明数组长度的指针就作为参数。


假定我们有一个叫做 calledmyFunction 的函数,接收一个地址和数字的数组作为参数,即 myFunction(address,uint256[])。该函数的函数签名是 0x4b294170。地址这一项,我们照上面所说的操作。因为我们的数组包含 3 个对象,数组的长度用十六进制表示为 0x3。然后每个对象都要占据恰好 32 自己的空间,且数组要放在所有其它参数之后,所以数组会从 32+32 = 64 字节之后开始。