参数编码和解码
本文主要介绍在Tron网络中触发智能合约调用时,如何进行参数的编码与解码,参数的编码解码遵循Solidity ABI编码规则。
ABI编码规则
本章节主要通过举例对ABI编码规则做简要介绍,详细的ABI编码规则,请参考Solidity官方文档中的ABI说明章节。
函数选择器(Function Selector)
一个合约函数调用的data
字段的前四个字节是函数选择器,指定了需要被调用的函数。
函数选择器是函数签名(Function Signature)进行Keccak-256运算后,左起的前四个字节。函数签名中只包含函数名和参数类型,没有参数名和空格。以tranfer(address _to, uint256 _value)为例,其函数签名是transfer(address,uint256)。
函数签名的Keccak-256哈希值可以通过调用 tronweb.sha3 接口得到。
参数编码
从第五个字节开始是函数调用的参数。这种编码规则在处理返回值和事件时同样适用。
参数类型
Solidity中有静态和动态两种参数类型。静态类型会被直接编码,动态类型则会在当前数据块之后单独分配的位置被编码。
- 静态参数:即定长参数,如uint256、bytes32、bool(布尔型是uint8,只能是0或1)。以uint256为例,对于一个类型是uint256的参数,即使值为1,也需要用0补齐到256位,即32字节,所以静态参数的长度是固定的,与值无关。
- 动态参数的长度是不定的。动态参数类型包括:bytes、string、任意类型 T 的变长数组T[]、任意动态类型 T 的定长数组 T[k] (k >= 0),以及由动态的 Ti (1 <= i <= k)构成的 元组tuple (T1,...,Tk)。
静态参数编码
示例一
function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
这个函数的签名是baz(uint32,bool),Keccak-256的计算结果是0xcdcd77c0992ec5bbfc459984220f8c45084cc24d9b6efed1fae540db8de801d2,其函数选择器就是0xcdcd77c0。
参数编码以十六进制的形式体现,每两个十六进制数位占一个字节。由于静态参数最长为256位,在编码时,每个静态参数长度都是256位,即32个字节,共64个十六进制数位,不足时左边用0补齐。
传递一组参数(69,true)给baz方法,其编码结果如下:
-
将十进制数69转成十六进制45,并在其左边补0,使其占32字节,结果是 0x0000000000000000000000000000000000000000000000000000000000000045
-
布尔型的true即为uint8的1,其十六进制值也是1,在左边补0,结果是 0x0000000000000000000000000000000000000000000000000000000000000001
编码完成后,用于传递的完整数据是:
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
示例二
同理,对于bytes型的静态类型数据,在编码时将其补足至32字节即可。不同的是,bytes型数据需要在右边补齐。以下面这个函数为例
function bar(bytes3[2] memory) public pure {}
函数签名bar(bytes3[2]),选择器0xfce353f6。
传递一组参数(abc,def)给这个函数,其编码结果如下:
- abc在ASCII表中分别对应97、98、99,转至十六进制为61、62、63。不足32字节用0在右边补齐,结果是 0x6162630000000000000000000000000000000000000000000000000000000000
- def在ASCII表中分别对应100、101、102,转至十六进制为64、65、66。不足32字节用0在右边补齐,结果是 0x6465660000000000000000000000000000000000000000000000000000000000
编码完成后,用于传递的完整参数是
0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000
动态参数编码
对于动态参数,由于其长度不定,需要首先用定长的offset来占位,并记录该动态参数实际的位置的偏移字节数,再对数据进行编码。
以f(uint,uint32[],bytes10,bytes)
这个函数为例,向其传递(0x123, [0x456, 0x789], "1234567890", "Hello, world!")这几个参数时,编码结果如下:
-
第一个静态参数编码:未标注长度的uint均视为uint256,0x123编码后结果是
0x0000000000000000000000000000000000000000000000000000000000000123
-
第二个动态参数的offset:对于uint32[]来说,由于数组长度是未知的,所以它是一个动态参数。首先用offset占位,offset记录的是这个参数起始位置的字节数。在这个uint32参数的正式编码前,分别有:第一个参数uint的编码(32字节)、第二个参数uint32[]的offset(32字节)、第三个参数bytes10的编码(32字节)、第四个参数bytes的offset(32字节),因此,实际该参数开始的位置的字节数应该是128,即0x80,编码结果是:
0x0000000000000000000000000000000000000000000000000000000000000080
-
第二个动态参数编码:对uint32[]传递了一个长度为2的数组[0x456, 0x789]。对于动态参数,首先要记录它的长度,即0x2,之后再对数值进行编码。这个参数的编码结果是
0000000000000000000000000000000000000000000000000000000000000002 0000000000000000000000000000000000000000000000000000000000000456 0000000000000000000000000000000000000000000000000000000000000789
-
第三个静态参数编码:"1234567890"是一个静态的bytes10参数,转至十六进制并用0补齐,结果是
0x3132333435363738393000000000000000000000000000000000000000000000
-
第四个动态参数的offset:对于最后一个bytes参数,由于其是动态类型,首先先用offset占位。在参数的实际内容之前的内容依次是:第一个参数uint的编码(32字节)、第二个参数uint32[]的offset(32字节)、第三个参数bytes10的编码(32字节)、第四个参数bytes的offset(32字节),第二个参数uint32[]的编码(96字节)。因此offset应该是224,即0xe0
0x00000000000000000000000000000000000000000000000000000000000000e0
-
第四个动态参数编码:对于bytes参数"Hello, world!",首先声明其长度13,即0xd。再将字符串转为十六进制字符,即0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000。这个参数编码结果是
000000000000000000000000000000000000000000000000000000000000000d 48656c6c6f2c20776f726c642100000000000000000000000000000000000000
至此所有参数都编码结束。最终传递的数据是
0x8be65246 - function selector 0000000000000000000000000000000000000000000000000000000000000123 - encoding of 0x123 0000000000000000000000000000000000000000000000000000000000000080 - offset of [0x456, 0x789] 3132333435363738393000000000000000000000000000000000000000000000 - encoding of "1234567890" 00000000000000000000000000000000000000000000000000000000000000e0 - offset of "Hello, world!" 0000000000000000000000000000000000000000000000000000000000000002 - length of [0x456, 0x789] 0000000000000000000000000000000000000000000000000000000000000456 - encoding of 0x456 0000000000000000000000000000000000000000000000000000000000000789 - encoding of 0x789 000000000000000000000000000000000000000000000000000000000000000d - length of "Hello, world!" 48656c6c6f2c20776f726c642100000000000000000000000000000000000000 - encoding of "Hello, world!"
参数编码与解码
了解了ABI编码规则后,那么就可以在代码中运用ABI规则进行参数的编码与解码了,TRON社区提供了许多SDK或者库供开发者使用,某些SDK已经封装好了参数的编码与解码,直接调用即可,例如trident-java,下面将通过trident-java 和JavaScript库来举例介绍如何在代码中进行参数的编码与解码。
参数编码
下面以USDT合约中的转账函数为例:
function transfer(address to, uint256 value) public returns (bool);
假设给地址412ed5dd8a98aea00ae32517742ea5289761b2710e转账50000 USDT,调用的triggersmartcontract接口如下:
curl -X POST https://127.0.0.1:8090/wallet/triggersmartcontract -d '{
"contract_address":"412dd04f7b26176aa130823bcc67449d1f451eb98f",
"owner_address":"411fafb1e96dfe4f609e2259bfaf8c77b60c535b93",
"function_selector":"transfer(address,uint256)",
"parameter":"0000000000000000000000002ed5dd8a98aea00ae32517742ea5289761b2710e0000000000000000000000000000000000000000000000000000000ba43b7400",
"call_value":0,
"fee_limit":1000000000,
"call_token_value":0,
"token_id":0
}'
上面的命令中parameter的值需要根据ABI规则进行编码。
使用Javascript进行参数编码示例
对于JavaScript,用户可以使用ethers库,下面是示例代码:
//建议使用ethers4.0.47版本
var ethers = require('ethers')
const AbiCoder = ethers.utils.AbiCoder;
const ADDRESS_PREFIX_REGEX = /^(41)/;
const ADDRESS_PREFIX = "41";
async function encodeParams(inputs){
let typesValues = inputs
let parameters = ''
if (typesValues.length == 0)
return parameters
const abiCoder = new AbiCoder();
let types = [];
const values = [];
for (let i = 0; i < typesValues.length; i++) {
let {type, value} = typesValues[i];
if (type == 'address')
value = value.replace(ADDRESS_PREFIX_REGEX, '0x');
else if (type == 'address[]')
value = value.map(v => toHex(v).replace(ADDRESS_PREFIX_REGEX, '0x'));
types.push(type);
values.push(value);
}
console.log(types, values)
try {
parameters = abiCoder.encode(types, values).replace(/^(0x)/, '');
} catch (ex) {
console.log(ex);
}
return parameters
}
async function main() {
let inputs = [
{type: 'address', value: "412ed5dd8a98aea00ae32517742ea5289761b2710e"},
{type: 'uint256', value: 50000000000}
]
let parameters = await encodeParams(inputs)
console.log(parameters)
}
main()
示例代码输出:
0000000000000000000000002ed5dd8a98aea00ae32517742ea5289761b2710e0000000000000000000000000000000000000000000000000000000ba43b7400
使用trident-java进行参数编码示例
trident中已经封装好了参数编码的过程,只需选择参数类型并传入参数值即可。参数的类型在org.tron.trident.abi.datatypes包中,请根据参数类型选择合适的java类。下面示例代码展示了如何使用trident生成合约的data信息。主要步骤如下:
- 构建Function对象,需要三个参数:函数名、入参和出参。详见Function代码。
- 调用FunctionEncoder.encode函数对function对象进行编码,生成合约交易的data数据。
public void SendTrc20Transaction() {
ApiWrapper client = ApiWrapper.ofNile("3333333333333333333333333333333333333333333333333333333333333333");
org.tron.trident.core.contract.Contract contr = client.getContract("");
// transfer(address,uint256) returns (bool)
Function trc20Transfer = new Function("transfer",
Arrays.asList(new Address("TVjsyZ7fYF3qLF6BQgPmTEZy1xrNNyVAAA"),new Uint256(BigInteger.valueOf(10).multiply(BigInteger.valueOf(10).pow(18)))),
Arrays.asList(new TypeReference<Bool>() {})
);
String encodedHex = FunctionEncoder.encode(trc20Transfer);
TriggerSmartContract trigger =
TriggerSmartContract.newBuilder()
.setOwnerAddress(ApiWrapper.parseAddress("TJRabPrwbZy45sbavfcjinPJC18kjpRTv8"))
.setContractAddress(ApiWrapper.parseAddress("TF17BgPaZYbz8oxbjhriubPDsA7ArKoLX3"))
.setData(ApiWrapper.parseHex(encodedHex))
.build();
System.out.println("trigger:\n" + trigger);
TransactionExtention txnExt = client.blockingStub.triggerContract(trigger);
System.out.println("txn id => " + Hex.toHexString(txnExt.getTxid().toByteArray()));
}
参数解码
在参数编码章节,调用了triggersmartcontract生成合约交易对象,然后签名并广播,等待交易成功上链后,可以通过gettransactionbyid获取链上的交易信息:
curl -X POST \
https://api.trongrid.io/wallet/gettransactionbyid \
-d '{"value" : "1472178f0845f0bfb15957059f3fe9c791e7e039f449c3d5a843aafbc8bbdeeb"}'
返回的结果如下:
{
"ret": [
{
"contractRet": "SUCCESS"
}
],
..........
"raw_data": {
"contract": [
{
"parameter": {
"value": {
"data": "a9059cbb0000000000000000000000002ed5dd8a98aea00ae32517742ea5289761b2710e0000000000000000000000000000000000000000000000000000000ba43b7400",
"owner_address": "418a4a39b0e62a091608e9631ffd19427d2d338dbd",
"contract_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c"
},
"type_url": "type.googleapis.com/protocol.TriggerSmartContract"
},
..........
}
返回值中的的raw_data.contract[0].parameter.value.data字段就是调用的transfer(address to, uint256 value) 函数及其参数。data字段的前面四个字节a9059cbb为函数选择器,来自ASCII格式的 transfer(address,uint256) 做 Keccak-256 运算后的前 4 字节,用于虚拟机对函数的寻址。后面的部分为参数,与参数编码章节中wallet/triggersmartcontract
接口中的parameter相同。
函数选择器,data的前四个字节,由Keccak-256得来,无法逆向推导。可以通过两种方法获取到函数签名:
- 如果可以获取到合约ABI,可以计算每个合约函数的选择器,并与data的前四个字节比对来判断函数
- 由合约生成的合约,链上可能没有ABI,合约部署者也可以通过
clearAbi
接口来清除链上ABI。当无法获取到ABI时,可尝试通过Ethereum Signature Database
来查询在数据库中的函数
对于参数的解码,请参考如下内容。
使用javascript进行参数解码示例
对data数据解码
下面的JavaScript代码是对data字段进行解码,获取出transfer函数传递的参数:
var ethers = require('ethers')
const AbiCoder = ethers.utils.AbiCoder;
const ADDRESS_PREFIX_REGEX = /^(41)/;
const ADDRESS_PREFIX = "41";
//types:参数类型列表,如果函数有多个返回值,列表中类型的顺序应该符合定义的顺序
//output: 解码前的数据
//ignoreMethodHash:对函数返回值解码,ignoreMethodHash填写false,如果是对gettransactionbyid结果中的data字段解码时,ignoreMethodHash填写true
async function decodeParams(types, output, ignoreMethodHash) {
if (!output || typeof output === 'boolean') {
ignoreMethodHash = output;
output = types;
}
if (ignoreMethodHash && output.replace(/^0x/, '').length % 64 === 8)
output = '0x' + output.replace(/^0x/, '').substring(8);
const abiCoder = new AbiCoder();
if (output.replace(/^0x/, '').length % 64)
throw new Error('The encoded string is not valid. Its length must be a multiple of 64.');
return abiCoder.decode(types, output).reduce((obj, arg, index) => {
if (types[index] == 'address')
arg = ADDRESS_PREFIX + arg.substr(2).toLowerCase();
obj.push(arg);
return obj;
}, []);
}
async function main() {
let data = '0xa9059cbb0000000000000000000000004f53238d40e1a3cb8752a2be81f053e266d9ecab000000000000000000000000000000000000000000000000000000024dba7580'
result = await decodeParams(['address', 'uint256'], data, true)
console.log(result)
}
示例代码输出:
[ '414f53238d40e1a3cb8752a2be81f053e266d9ecab', BigNumber { _hex: '0x024dba7580' } ]
对合约查询操作的返回值解码
以USDT合约中的查询函数为例:
balanceOf(address who) public constant returns (uint)
假设查询410583A68A3BCD86C25AB1BEE482BAC04A216B0261的余额,调用的triggercontractcontract接口如下:
curl -X POST https://127.0.0.1:8090/wallet/triggerconstantcontract -d '{
"contract_address":"419E62BE7F4F103C36507CB2A753418791B1CDC182",
"function_selector":"balanceOf(address)",
"parameter":"000000000000000000000041977C20977F412C2A1AA4EF3D49FEE5EC4C31CDFB",
"owner_address":"41977C20977F412C2A1AA4EF3D49FEE5EC4C31CDFB"
}'
返回结果如下:
{
"result": {
"result": true
},
"constant_result": [
"000000000000000000000000000000000000000000000000000196ca228159aa"
],
............
}
上面的返回值中constant_result就是balanceOf的返回值,下面是对constant_result解码的示例代码:
async function main() {
//必须是0x开头
let outputs = '0x000000000000000000000000000000000000000000000000000196ca228159aa'
//
//['uint256']是返回值类型的列表,如果有多个返回值,在按照顺序填写类型
result = await decodeParams(['uint256'], outputs, false)
console.log(result)
}
示例代码输出:
[ BigNumber { _hex: '0x0196ca228159aa' } ]
使用trident-java进行参数解码示例
对data数据解码
下面的Java代码是使用trident对data字段进行解码,获取出传递到transfer函数的参数的示例:
final String DATA = "a9059cbb0000000000000000000000007fdf5157514bf89ffcb7ff36f34772afd4cdc7440000000000000000000000000000000000000000000000000de0b6b3a7640000";
public void dataDecodingTutorial() {
String rawSignature = DATA.substring(0,8);
String signature = "transfer(address,uint256)"; //function signature
Address rawRecipient = TypeDecoder.decodeAddress(DATA.substring(8,72)); //recipient address
String recipient = rawRecipient.toString();
Uint256 rawAmount = TypeDecoder.decodeNumeric(DATA.substring(72,136), Uint256.class); //amount
BigInteger amount = rawAmount.getValue();
System.out.println(signature);
System.out.println("Transfer " + amount + " to " + recipient);
}
对合约查询操作的返回值解码
constant函数调用,将返回一个TransactionExtention对象,其中的constantResult字段就是查询结果,是一个List,将其转换为hex string后,可以使用上述示例代码中的TypeDecoder类可以对合约查询操作的返回值进行解码,还可以使用org.tron.trident.abi.FunctionReturnDecoder的decode方法:
在org.tron.trident.abi.FunctionReturnDecoder: decode方法中指定返回值的类型,它就可以将结果转换为这个类型的对象。
public BigInteger balanceOf(String accountAddr) {
//construct the funtion
Function balanceOf = new Function("balanceOf",
Arrays.asList(new Address(accountAddr)), Arrays.asList(new TypeReference<Uint256>() {}));
//call the function
TransactionExtention txnExt = wrapper.constantCall(Base58Check.bytesToBase58(ownerAddr.toByteArray()),
Base58Check.bytesToBase58(cntrAddr.toByteArray()), balanceOf);
//Convert constant result to human readable text
String result = Numeric.toHexString(txnExt.getConstantResult(0).toByteArray());
return (BigInteger)FunctionReturnDecoder.decode(result, balanceOf.getOutputParameters()).get(0).getValue();
}
Updated about 1 year ago