以太坊ABI介绍

  • A+
所属分类:区块链

ABI是什么

ABI全称 Application Binary Interface,字面意思是应用程序二进制接口,可以通俗的理解为合约的接口说明,当合约被编译后,它对应的abi也就确定了。
abi有点类似于程序中的接口文档,描述了字段名称、字段类型、方法名称、参数名称、参数类型、方法返回值类型等

为什么需要ABI

我们编写智能合约的流程是:

  • 编写合约代码(一般使用solidity语言)

  • 编译合约,将solidity编写的代码编译成EVM可识别的bytecode,这一步生成abi

  • 部署合约,将合约部署到区块链上,生成合约地址,将合约内容(即上一步生成的bytecode)作为input date输入。部署合约是一个交易过程,所以也会生成一个交易Has

  • 执行合约,获取合约地址,然后传入参数调用合约中的方法,获得执行结果

从上面的步骤可以看出,abi对于EVM来说,其实是不需要的。但是对于调用者来说,就需要知道合约有哪些方法,方法的参数是什么,返回值是什么,而这些信息就记录在智能合约的abi中。所以abi其实就相当于开发者的接口文档,方便开发者调用执行合约

ABI有哪些内容

我们来编写一个最简单的合约,然后编译生成abi看下内容

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.24;


contract Demo {

   uint private x;

   function set(uint _x) public {
       x = _x;
   }

}

执行 truffle compile 编译合约,就会生成对应的文件Demo.json,内容如下。由于json太长,删除了一些不重要的内容,完整内容可以自己手动执行看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
 "contractName": "Demo",
 "abi": [
   {
     "constant": false,
     "inputs": [
       {
         "name": "_x",
         "type": "uint256"
       }
     ],
     "name": "set",
     "outputs": [],
     "payable": false,
     "stateMutability": "nonpayable",
     "type": "function"
   }
 ],
 "bytecode": "0x6080604052348015600f57600080fd5b5060a48061001e6000396000f300608060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b1146044575b600080fd5b348015604f57600080fd5b50606c60048036038101908080359060200190929190505050606e565b005b80600081905550505600a165627a7a723058201dfe7c019fec67ccd87250c9ac8642c163cc5f43588715b33e8a8953df3715f60029",
 "deployedBytecode": "0x608060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b1146044575b600080fd5b348015604f57600080fd5b50606c60048036038101908080359060200190929190505050606e565b005b80600081905550505600a165627a7a723058201dfe7c019fec67ccd87250c9ac8642c163cc5f43588715b33e8a8953df3715f60029",
 "sourceMap": "27:97:1:-;;;;8:9:-1;5:2;;;30:1;27;20:12;5:2;27:97:1;;;;;;;",
 "deployedSourceMap": "27:97:1:-;;;;;;;;;;;;;;;;;;;;;;;;69:52;;8:9:-1;5:2;;;30:1;27;20:12;5:2;69:52:1;;;;;;;;;;;;;;;;;;;;;;;;;;;112:2;108:1;:6;;;;69:52;:::o",
 "source": "pragma solidity ^0.4.24;\n\n\ncontract Demo {\n\n    uint private x;\n\n    function set(uint _x) public {\n        x = _x;\n    }\n\n}\n",
 "sourcePath": "/Users/root/Workspace/DApp/demo/contracts/Demo.sol",
 "ast": {
   ...
 },
 "legacyAST": {
   ...
 },
 "compiler": {
   "name": "solc",
   "version": "0.4.24+commit.e67f0147.Emscripten.clang"
 },
 "networks": {},
 "schemaVersion": "2.0.1",
 "updatedAt": "2018-09-14T11:57:49.750Z"
}

大部分参数通过名称就可以看出来含义,这里我们主要介绍abi中各参数的含义和调用函数时生成ABI编码的过程

abi各参数的含义

  • name:函数名称

  • type:方法类型,包括functionconstructorfallback(缺省方法)可以缺省,默认为function

  • constant:布尔值,如果为true指明方法不会修改合约字段的状态变量

  • payable:布尔值,标明方法是否可以接收ether

  • stateMutability:状态类型,包括pure (不读取区块链状态),view (和constant类型,只能查看,不会修改合约字段),nonpayable(和payable含义一样),payable(和payable含义一样)。其实保留payableconstant是为了向后兼容

  • inputs:数组,描述参数的名称和类型

    • name:参数名称

    • type:参数类型

  • outputs:和inputs一样,如果没有返回值,缺省是一个空数组

这里要说明一点的是,由于示例中的x字段类型为private,所以没有生成一个和参数名称一样的函数,如果x字段类型为public,生成的abi就如下,会多一个和参数名称一样的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
"abi": [
   {
     "constant": true,
     "inputs": [],
     "name": "x",
     "outputs": [
       {
         "name": "",
         "type": "uint256"
       }
     ],
     "payable": false,
     "stateMutability": "view",
     "type": "function"
   },
   {
     "constant": false,
     "inputs": [
       {
         "name": "_x",
         "type": "uint256"
       }
     ],
     "name": "set",
     "outputs": [],
     "payable": false,
     "stateMutability": "nonpayable",
     "type": "function"
   }

上面我们讲了ABI中的参数含义,但是只有函数定义也是不行的,我们还需要调用,当调用一个函数时也需要对该函数进行编码,这样EVM才能执行,那么以太坊是如何生成可供EVM调用的字节码的。

生成的字节码主要分两部分:函数选择器和参数编码

函数选择器

即函数编码,对函数名称+参数类型进行sha3(keccak256)哈希运算之后,取前4个字节

1、方法一:
安装pyethereum [https://github.com/ethereum/pyethereum/#installation]

1
2
3
4
5
6
> from ethereum.utils import sha3
> sha3("set(uint256").hex()
'0x60fe47b16ed402aae66ca03d2bfc51478ee897c26a1158669c7058d5f24898f4'

> sha3("setA(uint256)")[0:4].hex()
'60fe47b1'

2、方法二:
打开Ganache,默认端口7545,然后再命令行执行以下命令

1
2
3
4
5
6
7
8
9
10
11
curl -X POST -i http://localhost:7545 --data '{
 "jsonrpc":"2.0",
 "method":"web3_sha3",
 "params":["set(uint256)"]
}'

返回结果:
{
 "jsonrpc": "2.0",
 "result": "0x60fe47b16ed402aae66ca03d2bfc51478ee897c26a1158669c7058d5f24898f4"
}

3、方法三:

1
2
3
4
const Web3 = require('web3')
const web3 = new Web3()
console.log(web3.sha3('set(uint256)'))
# 0x60fe47b16ed402aae66ca03d2bfc51478ee897c26a1158669c7058d5f24898f4

取前四个字节(一个字节=2个16进制字符)即:0x60fe47b1

参数编码

由于函数编码占用了4个字节,所以参数编码从第五位开始

参数的编码根据类型的不同,编码方式也有所区别。主要分为固定类型和动态类型

1、固定类型

  • uint:M为integer类型代表M bits,0 < M <= 256 , M % 8 == 0,如uint32,uint8,uint256。

  • int:同上。同为从8到256位的无符号整数。

  • uint和int:整型,分别是uint256和int256的别名。注意: 函数参数类型是uint,转sha3码时要变成uint256。

  • address:地址,20个字节,160bits。

  • bool:布尔类型,1个字节,true:1,false:0。高位补0

  • bytes:固定大小的字节数组,0<M<=32,byte都是bytes1的别名。

2、动态类型

  • bytes:动态分配大小字节数组。不是一个值类型!

  • string:动态大小UTF8编码的字符串,不是一个值类型!

  • T[] 某个类型的不定长数组

  • T[k] 某个类型的定长数组

编码规则

固定类型的编码就很简单,直接将参数值转成32字节长度的16进制即可。但是有区别的是:不足32bytes时,数字类型,如果是正数高位补0,如果是负数高位补1,布尔类型高位补0,字节类型、字符串类型在低位补全
动态类型的编码稍微复杂点,如果是固定长度就不需要计算偏移量,如果是不定长度就需要先计算偏移量,并在最后加上长度和具体值的编码,详细步骤下面会介绍

具体编码过程

介绍的例子和官方文档一样,如果理解有偏差可以查看源文档

1
2
3
4
5
6
7
pragma solidity ^0.4.16;

contract Foo {
 function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
 function bar(bytes3[2] memory) public pure {}
 function f(uint, uint32[], bytes10, bytes) public pure {}
}

案例

案例一:
函数:baz(bytes3[2] memory)
调用:baz(69, true)

  • 0xcdcd77c0,在node中使用new Web3().sha3('baz(uint32,bool)')生成

    1
    2
    3
    4
    const Web3 = require('web3')

    const web3 = new Web3()
    console.log(web3.sha3('f(uint256,uint32[],bytes10,bytes)'))
  • 0x0000000000000000000000000000000000000000000000000000000000000045,十进制69,转成16进制为45,因为是正数,高位补0至32bytes

  • 0x0000000000000000000000000000000000000000000000000000000000000001,bool类型,true=1false=0,高位补0
    所以最终字符串为:(换行显示是为了方便查看,实际是没有换行)

    1
    2
    3
    0xcdcd77c0
    0x0000000000000000000000000000000000000000000000000000000000000045
    0x0000000000000000000000000000000000000000000000000000000000000001

返回:该函数返回的是true,output将会是

1
0x0000000000000000000000000000000000000000000000000000000000000000

案例二:
函数:bar(bytes3[2] memory)
调用:bar(["abc", "def"])

  • 0xfce353f6,在node中使用new Web3().sha3('bar(bytes3[2])')生成

  • 固定长度不需要计算偏移量

  • 0x6162630000000000000000000000000000000000000000000000000000000000,字符串abc转成16进制后为616263,低位补0,字符串转16进制可以使用这个工具

  • 0x6465660000000000000000000000000000000000000000000000000000000000,同上
    所以最终字符串为:(换行显示是为了方便查看,实际是没有换行)

    1
    2
    3
    0xfce353f6
    0x6162630000000000000000000000000000000000000000000000000000000000
    0x6465660000000000000000000000000000000000000000000000000000000000

案例三:
函数:f(uint,uint32[],bytes10,bytes)
调用:f(0x123, [0x456, 0x789], "1234567890", "Hello, world!")

  • 0x8be65246,在node中使用f(uint256,uint32[],bytes10,bytes)生成

  • 0x00000000000000000000000000000000000000000000000000000000000001230x123对应的16进制,正数补全

  • 0x0000000000000000000000000000000000000000000000000000000000000080,动态类型,计算偏移量。这个的偏移量是指实际存储值的位置,由于这个函数有4个变量,那么实际存储值的位置就是第五个32bytes位置,也就是说偏移量等于4*32bytes=128,转成16进制后就是对应的值

  • 0x3132333435363738393000000000000000000000000000000000000000000000,字符串1234567890转成16进制后为31323334353637383930,bytes类型,低位补全

  • 0x00000000000000000000000000000000000000000000000000000000000000e0,动态类型,计算偏移量,这个偏移量就等于参数长度4*32bytes+前面的动态参数参数占有的长度(因为前面只有一个动态参数,所以这个长度就是1*32bytes+2*32bytes,1*32bytes是第一个动态参数长度所占的bytes数,2*32bytes是因为该函数中的第一个动态参数有2个值),那么具体的值就是 4*32bytes+(1*32bytes+2*32bytes)=7*32bytes=224,转成16进制就是e0,高位补全就是对应的值

  • 0x0000000000000000000000000000000000000000000000000000000000000002,第一个动态参数的长度,长度为2

  • 0x0000000000000000000000000000000000000000000000000000000000000456,第一个动态参数中的第一个元素

  • 0x0000000000000000000000000000000000000000000000000000000000000789,第一个动态参数中的第二个元素

  • 0x000000000000000000000000000000000000000000000000000000000000000d,第二个动态参数的长度,长度为13

  • 0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000,第二个动态参数的值编码
    所以最终字符串为:(换行显示是为了方便查看,实际是没有换行)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    0x8be65246
    0000000000000000000000000000000000000000000000000000000000000123
    0000000000000000000000000000000000000000000000000000000000000080
    3132333435363738393000000000000000000000000000000000000000000000
    00000000000000000000000000000000000000000000000000000000000000e0
    0000000000000000000000000000000000000000000000000000000000000002
    0000000000000000000000000000000000000000000000000000000000000456
    0000000000000000000000000000000000000000000000000000000000000789
    000000000000000000000000000000000000000000000000000000000000000d
    48656c6c6f2c20776f726c642100000000000000000000000000000000000000

以上就是以太坊调用函数时生成字节码的完整过程了!

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: