Ethereum 以太坊 交易数据 构建原理
当我们需要将某些数据写入区块链,这个写入的过程叫交易。从区块链中取数据叫调用Call。交易有别与传统数据库数据的写入,以太坊区块链需要先将写入的数据编码成16进制的字节码,区块链块中存储的基础类型如:Bytes32,Address.... 那么读取的过程则需要将16进制的字节码转换成utf8编码的字母或汉字,形成有价值意义的信息数据。
常见的交易如转账,Alice给Bob在以太坊链上转账ETH,下面是我在etherscan随便找了一笔ETH转账的交易回执
从回执数据上看,from是发送方,to是收款方,value是转账金额,这笔交易数据的构建还是很简单的。
如果在以太坊链上通过合约转账erc20的代币,那就需要针对合约中的转账方法构建一笔转账交易数据。
from仍然是发送方,而to则变成了该erc20代币的合约地址,具体的erc20转账数量则在input Data中。这笔交易数据最核心的点就在inputData中,以太坊执行合约代码需要依赖于evm虚拟机,而evm只能执行字节码bytecode,上面的input Data是整齐明了的字节码,看上去一目了然。
其中MethodID是函数transfer(address _to, uint256 _value)的函数签名,函数签名我在之前的文章中介绍过,并且也有方便的 ethSignUtil 脚本生成合约中的函数签名,后面两个参数值:000000000000000000000000d0292fc87a77ced208207ec92c3c6549565d84dd,
0000000000000000000000000000000000000000000000000de0b6b3a7640000,则是transfer函数的address收款方地址和value转账的erc20数量。erc20合约中一般转账交易数据就是 函数签名+收款方地址+转账数量。
最终的交易数据是这样的
0xa9059cbb
000000000000000000000000d0292fc87a77ced208207ec92c3c6549565d84dd
0000000000000000000000000000000000000000000000000de0b6b3a7640000
那这个交易数据为什么是这样?
以太坊的交易数据最小单位是32字节64位长度为一个值,如上方address地址数据:000000000000000000000000d0292fc87a77ced208207ec92c3c6549565d84dd,表示是一个地址类型的32字节码。一个address类型去掉前缀0x是40位长度,需要补全64位,在address数据左侧补0至总长为64位。
为什么是左则补零?
在evm执行字节码的约定中,凡是静态的基础类型则左补齐零至64长度。而动态类型则是右补齐零至64长度。
归纳下常见的静态类型:uint,bool,Address,bytes[0-32], 动态数组类型:bytes,string,address[],bytes32[].....
普通的转账构建的交易数据也是比较简单,而在合约开发中由于业务方法多种多样,可能会涉及静态数组,动态数组,那交易数据的构建就复杂多了。
下面看一个合约方法,参数涉及基本类型以及动态数组。
analysisHex(bytes,bool,uint256[],address,bytes32[]) 通过 ethSignUtil 脚本或者remix可以计算出函数签名:4b6112f8
如果需要向合约中这个方法analysisHex发送一笔交易数据,我们明文数据是这样的,"Alice",true,[9,8,7,6],"0x26d59ca6798626bf3bcee3a61be57b7bf157290e",["张三","Bob","老王"]
"Alice" 存储在bytes的动态数组中,true 用bool存储,[9,8,7,6] 存放在uint[]数组中,
"0x26d59ca6798626bf3bcee3a61be57b7bf157290e" 是一个address,最后 ["张三","Bob","老王"] 存储在bytes32的动态数组中。
那么构建这样一笔复杂的交易数据到底会经历什么?答案是: 当然只有你看下去才知道啦
function analysisHex(bytes name,bool b,uint[] data,address addr,bytes32[] testData) {}
首先我们先来分析这个函数参数字段,其中动态参数类型有:bytes,uint[],bytes32[],基础类型有:bool,address。动态类型参数在构建交易数据的过程中由于不确定参数值需要占几个bytes32的字节,则需要先占位。
记住动态数组类型需要先占位确定动态数组值的位置,而后根据位置补值。这样说可能很抽象,下面我将上述的明文值在remix中构建成交易数据。
0x4b6112f8 00000000000000000000000000000000000000000000000000000000000000a0 0000000000000000000000000000000000000000000000000000000000000001 00000000000000000000000000000000000000000000000000000000000000e0 00000000000000000000000026d59ca6798626bf3bcee3a61be57b7bf157290e 0000000000000000000000000000000000000000000000000000000000000180 0000000000000000000000000000000000000000000000000000000000000005 416c696365000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000004 0000000000000000000000000000000000000000000000000000000000000009 0000000000000000000000000000000000000000000000000000000000000008 0000000000000000000000000000000000000000000000000000000000000007 0000000000000000000000000000000000000000000000000000000000000006 0000000000000000000000000000000000000000000000000000000000000003 e5bca0e4b8890000000000000000000000000000000000000000000000000000 426f620000000000000000000000000000000000000000000000000000000000 e88081e78e8b0000000000000000000000000000000000000000000000000000
这个就是明文
"Alice",true,[9,8,7,6],"0x26d59ca6798626bf3bcee3a61be57b7bf157290e",["张三","Bob","老王"]
构建后的交易字节码数据。不要怕这么长的16进制字符串,你接着看,我负责给你讲明白。
下面是详细的动态数组交易数据构建的原理,主要复杂点是要确定动态数组值存储的位置, 我的github上也有详细的文档说明
函数方法
- function analysisHex(bytes name,bool b,uint[] data,address addr,bytes32[] testData) {}
构建交易的参数
- "Alice",true,[9,8,7,6],"0x26d59ca6798626bf3bcee3a61be57b7bf157290e",["张三","Bob","老王"]
函数签名
- 0x4b6112f8
32字节64位(方法签名索引从0开始,第0位位置是bytes动态数组,则先占位,值应该从第5个位置开始存储,32*5=160,160转成16进制a0,左补齐0至64位)
- 00000000000000000000000000000000000000000000000000000000000000a0
bool 在16进制中 0:false,1:true 静态类型,左补齐0至64位
- 0000000000000000000000000000000000000000000000000000000000000001
uint256[]是动态数组,它的占位由前面的参数决定,共5个参数,其中bytes动态类型值占了2个32字节长度,5+(1+1)=7从第7个位置开始uint256[]数组值存储的位置。32*7=224转换成16进制为e0,左补齐0至64位
- 00000000000000000000000000000000000000000000000000000000000000e0
address 类型是静态类型,去掉0x直接写入值,静态类型左补齐0至64位
- 00000000000000000000000026d59ca6798626bf3bcee3a61be57b7bf157290e
bytes32[]动态类型数组,需要由前面的参数决定值的位置,7+(1+4)=12,1个32位是确定uint数组的长度是4,然后是4个32位存放uint数组的值。32*12=384 转换成16进制是180,左补齐0至64位
- 0000000000000000000000000000000000000000000000000000000000000180
Alice的长度5,uint基础类型左补齐0至64位,Alice经过ASCII编码后值:416c696365,是动态类型,右补齐0至64位
- 0000000000000000000000000000000000000000000000000000000000000005
- 416c696365000000000000000000000000000000000000000000000000000000
uint256[] 数组[9,8,7,6],长度是4,基础类型uint左补齐0至64位)
- 0000000000000000000000000000000000000000000000000000000000000004
- 0000000000000000000000000000000000000000000000000000000000000009
- 0000000000000000000000000000000000000000000000000000000000000008
- 0000000000000000000000000000000000000000000000000000000000000007
- 0000000000000000000000000000000000000000000000000000000000000006
bytes32[] 数组的长度3,uint是基础类型左补齐0至64位,"张三" ASCII编码后值:e5bca0e4b889,"Bob" ASCII编码后值:426f62,"老王" ASCII编码后值:e88081e78e8b,动态数组bytes32[]值统统右补齐0至64位
- 0000000000000000000000000000000000000000000000000000000000000003
- e5bca0e4b8890000000000000000000000000000000000000000000000000000
- 426f620000000000000000000000000000000000000000000000000000000000
- e88081e78e8b0000000000000000000000000000000000000000000000000000
动态的交易数据类型由于不确定数组的长度,则需要占位,而静态类型的交易数据构建就比较简单些,直接在该参数所在的位置处存储值。我把上述的动态类型改成静态类型
function analysisHex(bytes32 name,bool b,uint[4] data,address addr,bytes32[3] testData) {}
那么它的函数签名也发生了变化,变成了:f8380e5f,那么我们同样传入明文
"Alice",true,[9,8,7,6],"0x26d59ca6798626bf3bcee3a61be57b7bf157290e",["张三","Bob","老王"]
它的交易数据会变成什么样呢?我在remix编码后得到
0xf8380e5f
416c696365000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000009
0000000000000000000000000000000000000000000000000000000000000008
0000000000000000000000000000000000000000000000000000000000000007
0000000000000000000000000000000000000000000000000000000000000006
00000000000000000000000026d59ca6798626bf3bcee3a61be57b7bf157290e
e5bca0e4b8890000000000000000000000000000000000000000000000000000
426f620000000000000000000000000000000000000000000000000000000000
e88081e78e8b0000000000000000000000000000000000000000000000000000
我们最终得到是 函数签名+5个参数对应的值,并没有动态数组的占位值。
相对于动态数组交易数据的构建,静态数组果然清净了很多,但是啊还是麻烦呀!
痛苦的过来者在没有使用web3j之前都是拼呀拼呀拼接16进制的字符串构建一笔笔交易数据,下面看看web3j是如何构建交易数据的,附上web3j构建交易数据的核心源码,并根据上述的动态数组交易数据添加注释说明
//web3j 生成以太坊函数签名核心的两个方法解析 public static String encode(Function function) { //获取所有的方法参数集合 List<Type> parameters = function.getInputParameters(); //组装函数方法名和参数类型:如:analysisHex(bytes,bool,uint256[],address,bytes32[]) String methodSignature = buildMethodSignature(function.getName(), parameters); //使用Keccak-256生成函数签名,0x4b6112f8 String methodId = buildMethodId(methodSignature); //result 表示最终返回的函数签名+参数字节码,rsult由两部分组成:参数位置/参数字节码值 + 动态数组的字节码值 StringBuilder result = new StringBuilder(); //追加函数签名 result.append(methodId); //参数签名方法 return encodeParameters(parameters, result); } private static String encodeParameters(List<Type> parameters, StringBuilder result) { //获取所有参数需要占用的字节存储位置数,如:analysisHex(bytes,bool,uint256[],address,bytes32[]),需要5*32个字节的位置,另外三个动态数组的值在后面补充 int dynamicDataOffset = getLength(parameters) * Type.MAX_BYTE_LENGTH; //动态数组参数值的字节串 StringBuilder dynamicData = new StringBuilder(); for (Type parameter:parameters) { //获取每个参数的字节码,如果是动态数组则需要多加一个32位字节的该数组长度值,告诉evm该动态数组需要存储N个32位字节 String encodedValue = TypeEncoder.encode(parameter); //如果是动态数组 if (TypeEncoder.isDynamic(parameter)) { //计算动态数组具体值所在的位置,如:第一个参数动态数组bytes,它的值存储的位置是从索引5开始的,即32*5=160,转换成16进制就是a0 String encodedDataOffset = TypeEncoder.encodeNumeric( new Uint(BigInteger.valueOf(dynamicDataOffset))); //result追加动态数组值所在的位置字节码 result.append(encodedDataOffset); //动态数组参数值追加 dynamicData.append(encodedValue); //计算当前动态数组参数值已使用到若干字节位置, encodedValue.length() >> 1 表示encodedValue的长度占了多少个32位字节 dynamicDataOffset += encodedValue.length() >> 1; } else { //非动态数组直接追加参数字节码的值 result.append(encodedValue); } } //result追加所有的动态数组参数字节码的值 result.append(dynamicData); return result.toString(); }
构建交易数据是区块链发送交易的前提,这些数据还都是未签名的,没有身份归属。需要发送者用私钥进行签名,签名后的数据进入交易池等待矿工的验证,待签名的数据验证通过,这笔交易才算打包入链。
本篇只是介绍交易数据的准备阶段,后续的交易签名包括钱包常用的离线签名、私钥托管的在线签名以及验签,在后续的文章中会介绍。如果本篇的交易数据原理构建对你认识以太坊的交易有了新的高度。那就赶紧关注我吧!持续“刻意链习”!欢迎转载,分享区块链知识,转载请注明出处!
更多帮助
交易数据构建文档说明:
https://github.com/zhjgit/Ethereum-util/blob/master/dynamicDataExplain.md
web3j交易数据构建的核心源码:
https://github.com/web3j/web3j/blob/master/abi/src/main/java/org/web3j/abi/FunctionEncoder.java