[关闭]
@Fangzheng1992 2018-11-13T08:43:43.000000Z 字数 5957 阅读 1012

以太坊ABI介绍

Calldata介绍

由于ABI主要约定了Calldata的编码方式,所以首先简单介绍一下EVM存储和Calldata。EVM能够访问的存储空间主要有四类:

Calldata编码

Calldata编码格式由以太坊ABI规范文档约定。简单来说,Calldata包含两部分数据:

ABI具体支持的参数类型和编码方式请参考ABI规范文档,这里要说明的是,Calldata的编码格式并不是自描述的,所以必须要了解函数签名,或者有描述文件才能对Calldata进行解码。换句话说,Calldata的编码方式更像Protobuf而非JSON

ABI生成

编译Solidity智能合约时,可以通过--abi选项告诉编译器生成ABI描述。比如下面给出ABI规范里的一个例子:

  1. pragma solidity >=0.4.16 <0.6.0;
  2. contract Foo {
  3. function bar(bytes3[2] memory) public pure {}
  4. function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
  5. function sam(bytes memory, bool, uint[] memory) public pure {}
  6. }

把上面的智能合约代码保存在foo.sol文件里,用solc --abi foo.sol命令编译该文件,可以在控制台看到输出的ABI描述(JSON格式):

  1. $ solc --abi foo.sol
  2. ======= foo.sol:Foo =======
  3. Contract JSON ABI
  4. [
  5. {
  6. "constant":true,
  7. "inputs":[
  8. { "name":"", "type":"bytes" },
  9. { "name":"", "type":"bool" },
  10. { "name":"", "type":"uint256[]" }
  11. ],
  12. "name":"sam",
  13. "outputs":[],
  14. "payable":false,
  15. "stateMutability":"pure",
  16. "type":"function"
  17. },
  18. {
  19. "constant":true,
  20. "inputs":[
  21. { "name":"x", "type":"uint32" },
  22. { "name":"y", "type":"bool" }
  23. ],
  24. "name":"baz",
  25. "outputs":[
  26. { "name":"r", "type":"bool" }
  27. ],
  28. "payable":false,
  29. "stateMutability":"pure",
  30. "type":"function"
  31. },
  32. {
  33. "constant":true,
  34. "inputs":[
  35. { "name":"", "type":"bytes3[2]" }
  36. ],
  37. "name":"bar",
  38. "outputs":[],
  39. "payable":false,
  40. "stateMutability":"pure",
  41. "type":"function"
  42. }
  43. ]

可以看到,ABI中包含了函数名,参数数量和类型,返回值数量和类型等信息。当通过交易调用合约时,需要ABI描述来把被调函数名称和参数列表编码成Calldata数据。

合约调用

前面我们简单介绍了Solidity编译器(solc)的--abi选项,这里再介绍几个其他选项:

仍以foo.sol为例,下面是用--bin-runtime选项进行编译的输出结果:

  1. $ solc --bin-runtime foo.sol
  2. ======= foo.sol:Foo =======
  3. Binary of the runtime part:
  4. 608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063a5643bf21461005c578063cdcd77c014610114578063fce353f61461016b575b600080fd5b34801561006857600080fd5b50610112600480360381019080803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509192919290803515159060200190929190803590602001908201803590602001908080602002602001604051908101604052809392919081815260200183836020028082843782019150505050505091929192905050506101bd565b005b34801561012057600080fd5b50610151600480360381019080803563ffffffff1690602001909291908035151590602001909291905050506101c2565b604051808215151515815260200191505060405180910390f35b34801561017757600080fd5b506101bb60048036038101908080604001906002806020026040519081016040528092919082600260200280828437820191505050505091929192905050506101de565b005b505050565b600060208363ffffffff1611806101d65750815b905092915050565b505600a165627a7a72305820547efbdaf31172da7c479533ce74ac5c90219733d7d14406cd5d836bd111ab130029

不管是hex、Opcode还是ASM形式,都不是很容易就能理解其中的内容。我们把上面的hex字节码输入Solidity在线反编译器,可以得到更直观易懂的Solidity代码:

  1. contract Contract {
  2. function main() {
  3. memory[0x40:0x60] = 0x80;
  4. if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }
  5. var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff;
  6. if (var0 == 0xa5643bf2) {
  7. // Dispatch table entry for 0xa5643bf2 (unknown)
  8. var var1 = msg.value;
  9. if (var1) { revert(memory[0x00:0x00]); }
  10. var1 = 0x0112;
  11. var temp0 = msg.data[0x04:0x24] + 0x04;
  12. var temp1 = msg.data[temp0:temp0 + 0x20];
  13. var temp2 = memory[0x40:0x60];
  14. memory[0x40:0x60] = temp2 + (temp1 + 0x1f) / 0x20 * 0x20 + 0x20;
  15. memory[temp2:temp2 + 0x20] = temp1;
  16. memory[temp2 + 0x20:temp2 + 0x20 + temp1] = msg.data[temp0 + 0x20:temp0 + 0x20 + temp1];
  17. var var2 = temp2;
  18. var var3 = !!msg.data[0x24:0x44];
  19. var temp3 = msg.data[0x44:0x64] + 0x04;
  20. var temp4 = msg.data[temp3:temp3 + 0x20];
  21. var temp5 = memory[0x40:0x60];
  22. memory[0x40:0x60] = temp5 + temp4 * 0x20 + 0x20;
  23. memory[temp5:temp5 + 0x20] = temp4;
  24. var temp6 = temp4 * 0x20;
  25. memory[temp5 + 0x20:temp5 + 0x20 + temp6] = msg.data[temp3 + 0x20:temp3 + 0x20 + temp6];
  26. var var4 = temp5;
  27. func_01BD(var2, var3, var4);
  28. stop();
  29. } else if (var0 == 0xcdcd77c0) {
  30. // Dispatch table entry for baz(uint32,bool)
  31. var1 = msg.value;
  32. if (var1) { revert(memory[0x00:0x00]); }
  33. var1 = 0x0151;
  34. var2 = msg.data[0x04:0x24] & 0xffffffff;
  35. var3 = !!msg.data[0x24:0x44];
  36. var1 = baz(var2, var3);
  37. var temp7 = memory[0x40:0x60];
  38. memory[temp7:temp7 + 0x20] = !!var1;
  39. var temp8 = memory[0x40:0x60];
  40. return memory[temp8:temp8 + (temp7 + 0x20) - temp8];
  41. } else if (var0 == 0xfce353f6) {
  42. // Dispatch table entry for 0xfce353f6 (unknown)
  43. var1 = msg.value;
  44. if (var1) { revert(memory[0x00:0x00]); }
  45. var1 = 0x01bb;
  46. var temp9 = memory[0x40:0x60];
  47. memory[0x40:0x60] = temp9 + 0x20 * 0x02;
  48. memory[temp9:temp9 + 0x20 * 0x02] = msg.data[0x04:0x44];
  49. var2 = temp9;
  50. func_01DE(var2);
  51. stop();
  52. } else { revert(memory[0x00:0x00]); }
  53. }
  54. function func_01BD(var arg0, var arg1, var arg2) {}
  55. function baz(var arg0, var arg1) returns (var r0) {
  56. var var0 = 0x00;
  57. var var1 = arg0 & 0xffffffff > 0x20;
  58. if (var1) { return var1; }
  59. else { return arg1; }
  60. }
  61. function func_01DE(var arg0) {}
  62. }

从上面的反编译结果可以看到,编译后的智能合约字节码,实际上有一个入口函数main()函数)。当EVM执行智能合约时,实际上会先进入这个入口函数。然后入口函数负责解码Calldata,取出函数签名哈希,选择被调函数,并传入解码后的参数值。

总结

  1. 调用合约时,需要给定函数名和参数列表。函数名和参数列表通过ABI约定的格式编码成Calldata数据提供给合约,合约字节码可以通过特定的指令访问只读的Calldata数据。
  2. Calldata编码格式是非自描述的,需要了解参数数量和类型才能解码Calldata数据。
  3. 编译合约时,可以通过--abi选项输出合约的ABI描述;ABI描述采用JSON格式。
  4. Calldata解码逻辑(以及函数分派逻辑)是由Solidity编译器生成的,硬编码在运行时字节码里,因此合约执行时,只需要Calldata即可,并不需要ABI描述文件。

本文由Wormhole团队张秀红创作,转载无需授权

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注