在ethereum交易签名中总结了以太坊交易的构成方式,不论是合约调用还是普通的ETH
转账交易,交易的构造方式都是一样的。在转ETH的交易的中,目标地址to
是我们将要转入的账户地址,针对合约调用,目标地址to
是我们将要调用合约的地址。要构造出这两种交易,关键是交易中data
字段的构造;
ETH转账交易data构造
根据以太坊开发文档,我们可以在ETH转账的时候填写一定长度的附加信息做为本次交易的备注信息。在钱包转账交易中,只需要将用户填写的备注信息转换为Vec<u8>
格式并将结果赋值给data
字段即可;当交易成功后,可以在etherscan中查询到当次交易添加的备注信息。
合约调用data字段构造
当交易为合约调用时,data字段的内容将来至于两部分,其中是一部分数据来自于合约调用方法编码,另外一部分数据来至于交易的备注信息。通过这种方式实现了用户在合约调用时,添加备注信息的需求。他们的构造关系如下所示:
1 | //合约方法、参数编码 |
上述代码表示用户在调用erc20合约转账方法时参数拼接方式。其中get_erc20_transfer_data
主要功能是通过调用如下代码实现:
1 | fn encode_contract_input<P>(&self, method: &str, params: P) -> Result<Vec<u8>, error::Error> where P: contract::tokens::Tokenize { |
下面内容重点描述encode_contract_input
实现原理。
合约方法、参数编码
当需要进行合约调用时,我们需要获取目标合约对应的ABI
文件,ABI
文件中有关于合约方法、所需参数以及参数类型的描述,通过一系列编码规则,最终会得到我们所需要的目标数据。整个编码数据是将方法编码和参数编码两部分参数组合起来的字节数组。使用下面的合约来详细说明编码的过程:
1 | pragma solidity ^0.4.16; |
方法编码
获取将要调用方法对应的签名,以方法baz
为例,它对应的函数签名为baz(uint32,bool)
,对函数使用Keccak
算法做Hash
运算,取运算结果的前4个字节作为标识符来代表将要调用的方法。
关键代码如下所示:
1 | fn fill_signature(name: &str, params: &[ParamType], result: &mut [u8]) { |
针对baz(uint32,bool)
的计算结果为0xcdcd77c0
。(这里取前4个字节的原因估计在定义的合约方法中有足够大的概率避免hash冲突又能节省空间)。
数据编码
与方法编码相比较,数据编码就显得复杂的多。在合约参数定义中,存在静态类型数据和动态类型,这里将uint<M>、int<M>,bool,address
定义为静态类型,其中0 < M <= 256&&M % 8 == 0
代表暂用的字节数;动态数据类型定义比如:bytes、string、T[]、T[k]、(T1,…,Tk),其中T为任意类型;
针对静态类型除address
占用20字节外,另外的静态类型都用32字节表示,通过在左边补零的方式补齐。
以调用baz方法,传入参数69
,true
为例;
- 69 编码为十六进制为0x45,通过移位补齐32字节的方式,最后编码的结果为:
0x0000000000000000000000000000000000000000000000000000000000000045
; - true,最后编码的结果的为
0x0000000000000000000000000000000000000000000000000000000000000001
;
所以针对baz(69,true)
最后编码出来的结果的为:0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
;
针对同时包含动态类型和静态类型的方法,遵循静态数据直接编码存放
,动态类型数据存放位置索引
,通过索引来指示实际的编码数据。
以编码sam
方法为例: sam("dave", true 、[1,2,3])
,该方法中包含两个动态类型,针对第一个参数,先传入真实数据的索引位置,是从第96
(后面有详细的计算说明为什么是这个数)个字节位置开始,计算规则如下:
- 第一个参数索引位置表示 占32字节;
- 第二个参数为静态类型,直接表示为
0x0000000000000000000000000000000000000000000000000000000000000001
,占用32个字节; - 第三个参数,为动态类型,直接使用32个字节表示索引。
因此第一个参数的真实数据是从(32+32+32 = 96)位置开始的。将所有的参数都表示完后,后面就开始添加动态类型数据。因为动态类型数据长度是动态变化的,所以真实数据开始前还需要有长度声明的字段,这个字段也是32字节的大小;
- “dave”的长度为4,编码后的数据为
0x0000000000000000000000000000000000000000000000000000000000000004
; - 针对
dave
直接转换为十六进制为0x64617665
,不满足32字节的大小,在后续通过补零的方式补齐,最后的编码的结果为0x6461766500000000000000000000000000000000000000000000000000000000
; - 通过上面的描述,能够计算出来第三个参数的其实位置索引为:(96+64=160),即编码后的数据为
0x00000000000000000000000000000000000000000000000000000000000000a0
, [1,2,3]
是动态类型数据,先编码数组的长度为0x0000000000000000000000000000000000000000000000000000000000000003
;
数组中对应的成员对应的编码分别为
0x0000000000000000000000000000000000000000000000000000000000000001
;0x0000000000000000000000000000000000000000000000000000000000000002
;0x0000000000000000000000000000000000000000000000000000000000000003
;
综合上面的编码数据,sam("dave", true 、[1,2,3])
最后编码的结果为0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
通过上面合约方法、参数编码的介绍,将编码后的结果与用户的备注信息拼接,就完成了构造交易结构中data
字段的构造。在上述编码的过程中,我们可以发现它的编码规则,当编码的数据为字符串类型时,数据直接通过右补零的方式补齐32字节的大小,当数据为数值型时,是通过在左边添加零偏移的方式补齐32字节大小空间。关于合约编码更详细的内容可以参考文档.
总结
在最近开发钱包过程中,这部分的功能已经编码结束已经有一段时间了,当时为了实现在转ERC20时同时实现交易备注信息的添加,在网上查了相关资料都没有看到文档说明怎么来处理合约调用编码数据与用户备注信息的关系,今天算是对这部分知识的复习。