我们都知道,乙太坊中的合约其实就是一段程序,一般由solidity开发。
在这一段程序中,定义了:
一系列的变量,用来做合约本地存储,一般只在合约内部被调用,也可以声明成public的,solidity编译器会自动为其生成对应的get函数。
一个构造函数,仅在合约部署时被被自动调用一次,用来初始化合约;
很多方法,其中只有public或external的方法才可以被外界调用,用户主要面向的是这些函数。
所谓的调用合约,其实就是调用合约中的public或external方法。
下面是一个简单的合约,就是实现了一个给一个整数乘于7的函数:
contract Multiply7 { event Print(uint); function multiply(uint input) returns (uint) { Print(input * 7); return input * 7; } }
这个合约在编译后,就会生成一段二进制字节码,一个abi文件(合约描述文件,这个文件后面会用到)。
既然要调用方法,那么就需要知道合约的地址,方法名,参数列表。
我们以执行multiply(6)来举例:
合约地址:xxxxxxxxxxxxxx这个就说了
方法名: multiply
函数:6
如果通过rpc接口来调用,就是以下的语法:
curl --data '{"jsonrpc":"2.0","method": "eth_sendTransaction", "params": [{"from": "0xeb85a5557e5bdc18ee1934a89d8bb402398ee26a", "to": "0x6ff93b4b46b41c0c3c9baee01c255d3b4675963d", "data": "0xc6888fa10000000000000000000000000000000000000000000000000000000000000006"}], "id": 8}' localhost:8545
我们发现,这个接口调口的参数params中,除了from代表合约调起人,to为合约地址,还有一个不知何物的data,没有出现multiply和6,这是为什么?
其实,玄机就隐藏在data中:
"data": "0xc6888fa10000000000000000000000000000000000000000000000000000000000000006"
这是对调用数据的16进制编码,而这个16进制编码正是evm所需要的。我们先不管evm怎么处理这段编码,先来看看这段编码是怎么生成的。
说这个之前,就要先说一下合约的abi,即Application Binary Interface Specification,“接口定义说明”。
它会用json的形式描述一个合约中所有方法,以这个合约为例,它的abi就是下面的样子:
[{
"constant": false,
"inputs": [{
"name": "input",
"type": "uint256"
}],
"name": "multiply",
"outputs": [{
"name": "",
"type": "uint256"
}],
"type": "function"
}, {
"anonymous": false,
"inputs": [{
"indexed": false,
"name": "",
"type": "uint256"
}],
"name": "Print",
"type": "event"
}]
可以看到,这个文件描述了方法multiply和事件Print,它们的方法名,输入参数和输入结果等等。
那么上面的16进制编码
0xc6888fa10000000000000000000000000000000000000000000000000000000000000006
是怎么生成的呢?
(1)生成函数签名:
通过abi定义,找到multiply的方法和它的参数类型列表(unin256),组成函数签名 multiply(unin256),其实这个函数签名就是类似一个函数声明的样子,只不过,只有函数名和参数类型的列表。
(2)对函数签名进行hash:
sha3("multiply(uint256)").substring(0, 8)该函数对hash结果取了前4个字节,也就是32位,表现出来就是8位的16进制字符串,也就是
c6888fa1
(3)对参数值进行编码:
我们的参数是6,需要将其转化为32位的字节码,表现为16进制的字符串就是:
0000000000000000000000000000000000000000000000000000000000000006
(4)将2和3的结果组合起来,就完成了函数调用的编码了。
这段逻辑的实现,在乙太坊源码中,是如下的位置:
accounts/abi/bind/base.go的Call方法中:
(1)method和params进行编码:
input, err := c.abi.Pack(method, params...)
(2)组装调用数据结构体:
msg = ethereum.CallMsg{From: opts.From, To: &c.address, Data: input}
(3)调用合约:
output, err = c.caller.CallContract(ctx, msg, nil)
好了,以上就是客户端发起合约调用的过程。