Solidity原理(三):abi编码以及与EVM交互的原理

1.   如何生成smart contract对应的ABI(ApplicationBinary Interface)

函数的abi由以下元素组成

1)     Type:有“function”,“constructor”, “fallback”三种

2)     Name:函数名

3)     Inputs:函数的参数类型,参数名,components(tuple类型才会有)

4)     Output:函数的返回值,名字(可省略)

5)     Payable:函数是否是payable的

6)     StateMutability:pure/view/nonpayable/payable 这四种的一种

7)     Constant:如果是pure或者view,则为true,否则为false

Event的abi由以下元素组成:

1) Type:“event”

2) Name:event的名字

3) Inputs:event的参数类型,参数名,components(tuple类型才会有),indexed(参数是否被indexed关键字修饰,indexed会给该参数一个编码,方便事件过滤)

4) Anonymous:是否声明为anonymous,如果声明为anonymous, 事件的签名不会被保存为topic,也就是说无法通过事件名称过滤该事件

 

从abi的生成规则来看,可以直接通过源码来获得abi,以下是个合约对应的abi

pragma solidity ^0.4.0;
contract Test {
  function Test() public { b = 0x12345678901234567890123456789012; }
  event Event(uint indexed a, bytes32 b);
  event Event2(uint indexed a, bytes32 b);
  function foo(uint a) public { Event(a, b); }
  bytes32 b;
}

Solidity原理(三):abi编码以及与EVM交互的原理Solidity原理(三):abi编码以及与EVM交互的原理Solidity原理(三):abi编码以及与EVM交互的原理Solidity原理(三):abi编码以及与EVM交互的原理Solidity原理(三):abi编码以及与EVM交互的原理Solidity原理(三):abi编码以及与EVM交互的原理Solidity原理(三):abi编码以及与EVM交互的原理

2-14行对应的是函数foo,16-21是构造函数的abi,22-38,39-56分别是事件Event, Event2对应的abi

 

通过Web3,Remix等工具,可以直接生成合约的abi

 

 

 

2.   使用ABI调用合约函数,并获取返回值的原理

在使用ABI调用合约函数时,传入的ABI会被编码成calldata(一串hash值,编译过程见3.)。calldata由function signature和argument encoding两部分组成。通过读取call data的内容, EVM可以得知需要执行的函数,以及函数的传入值,并作出相应的操作。

Call Data: 是除了storage,memory的另一个数据保存位置,保存了inputdata。长度是4bytes+32bypes*n.可通过CALLDATALOAD,CALLDATASIZE, CALLDATACOPY等指令进行操作

EVM读取并执行call data的规则如下:

a.      函数选择器: Call data的前4个bytes对应了合约中的某个函数。因此EVM通过这4个bytes, 可以跳转到相应的函数。

ByteCode中的体现:

  Solidity原理(三):abi编码以及与EVM交互的原理Solidity原理(三):abi编码以及与EVM交互的原理Solidity原理(三):abi编码以及与EVM交互的原理Solidity原理(三):abi编码以及与EVM交互的原理

上面左边两张图是合约bytecode最开始的部分,EVM依次执行每条命令,当执行到CALLDATASIZE(L6)时,EVM读取input的size(读取的是function的hash, 长度为4bytes)。L7会和4进行比较,作为L9的JUMPI的跳转条件

l  input的size比4小,则跳转到fallback function(fallback function是唯一一个没有名字的函数)。跳转的地址是由L8 push到栈中的,跳转的地址是6d,转换成10进制为109,也就是L42。

l  input的size长度为4,则会继续执行每个指令。L11的CALLDATALOAD会从读取input的具体值,与L18,23,28,33,38的hash值(该合约中的函数签名)进行比较,以L18为例,L19会比较input的函数签名与L18的哈希值是否相等。如果相等,则跳转到L20表明的地址(PC:8d),否则继续执行,直到遇到函数签名相等的情况为止

l  如果没有一个函数签名相等(此时执行到L41),因为没有执行到终止命令,如stop。EVM会继续往下执行L42,L42是fallback function的起始位置。因此fallback function的执行情况是:

A contractcan have exactly one unnamed function. This function cannot have arguments andcannot return anything. It is executed on a call to the contract if none of theother functions match the given function identifier. Furthermore, this functionis executed whenever the contract receives plain Ether (without data). 

   

functionchangeState(bool newState, uint value) returns(uint) {

              value++;

              GambleState = newState;

              return value;

}

Solidity原理(三):abi编码以及与EVM交互的原理Solidity原理(三):abi编码以及与EVM交互的原理Solidity原理(三):abi编码以及与EVM交互的原理Solidity原理(三):abi编码以及与EVM交互的原理

b.      参数读取:

call data是32bytes的整数倍(头4 bytes的函数签名除外),EVM通过CALLDATALOAD指令,每次能从call data中读取32byte的值,放入stack中。上面的图通过2个CALLDATALOAD分别读出了newState和value的值,放入了stack中。通过JUMP指令跳转到函数体(tag19),并继续执行。

 

c.      返回值:

当函数结束完时,会跳转到tag18。Tag 18最末尾的RETURN指令,下面是RETURN指令的实现。RETURN指令会从stack中读取两个值,通过这两个值从memory中读取相应的值,以返回给调用方。DAPP端具体如何获得RETURN值在4中回答

Solidity原理(三):abi编码以及与EVM交互的原理Solidity原理(三):abi编码以及与EVM交互的原理

 

3.   ABI是如何编码的

a.      函数编码:在EVM中,每个函数都由4个byte长度的16进制值来唯一标识,这4个bytes叫做函数签名。函数签名是对函数名,函数参数做Keccak(SHA-3) 运算后,获得的hash值的前4个bytes.

如下面increaseAge这个函数,直接通过web3提供的sha3,取前4bytes即可获得函数签名F9EA5E79. EVM会通过这个函数签名找到对应的函数,通过JUMPI跳转到对应的函数。

functionincreaseAge(string name, uint num)returns (uint){

       return ++age;

}

Solidity原理(三):abi编码以及与EVM交互的原理Solidity原理(三):abi编码以及与EVM交互的原理Solidity原理(三):abi编码以及与EVM交互的原理Solidity原理(三):abi编码以及与EVM交互的原理

b.      函数参数编码:

每个参数都是以一个32byte长度hash值的形式传入的,长度不够用0补,如uint8的长度是8bytes,前面不足的24bytes都用0来补。用下面的合约来举例:

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]) public pure {}
  function sam(bytes, bool, uint[]) public pure {}
}

1) baz函数:0xcdcd77c0是bar的函数签名,如果要传入69和true两个参数,69首先会被编码成0x0000000000000000000000000000000000000000000000000000000000000045,true则被编码成0x0000000000000000000000000000000000000000000000000000000000000001,最后总的传入参数为0xcdcd77c0 0000000000000000000000000000000000000000000000000000000000000045 0000000000000000000000000000000000000000000000000000000000000001,一共68bytes。

这个函数会返回一个bool类型的值,假设返回值是false,那么在调用方会收到

0x0000000000000000000000000000000000000000000000000000000000000000

2) bar函数:假设要传入的值为[“abc”,”def”],”abc”对应的hash值为0x6162630000000000000000000000000000000000000000000000000000000000,”def”对应的值为0x6465660000000000000000000000000000000000000000000000000000000000,bytes采用的是左端对齐的方式,以区分uint

3) sam函数:存在两个动态类型bytes和uint[],对于动态类型在编码时,会先存入一个32bytes的值,代表这个动态类型的长度。比如要传入[1,2,3],会先编码一个0000000000000000000000000000000000000000000000000000000000000003,表示接下来有3个uint的值

 

4.   如何使用abi与smart contract进行交互

Web3是以太坊提供的一个库,提供了Python,js,java等版本。它能够在DAPP端使用ABI与smart contract进交互(调用合约以及获得返回值内容)。换句话说,当dapp端调用smart contract的某个函数时,web3的作用就是把abi通过网络发送给Ethereum的node。Node接收到abi之后,编译成hash值并且执行。Node把执行完的结果上传到区块链。如果有返回值,再Node通过网络的方式返回给DAPP。

DAPP端在与合约进行交互之前,需要与以太坊进行连接,连接方式有IPC与RPC。

IPC(Inter-process Communications)在使用前必须同步数据,因为该模式下是通过本地的数据进行运行的,所以返回值直接同本地获得。使用方法:同步完数据后,开启geth ipc。在dapp端连接ipc服务后,与smart contract进行交互。

RPC(Remote Procedure Call)通过网络连接,与远程的Ethereum node进行交互。执行的结果通过网络发送。使用方法:geth --rpc --rpcaddr<ip> --rpcport <portnumber> 命令开启rpc服务,在dapp端连接到ip:portnumber之后,即可用web3与smart contract进行交互。