参数编码和解码

本文主要介绍在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信息。主要步骤如下:

  1. 构建Function对象,需要三个参数:函数名、入参和出参。详见Function代码
  2. 调用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();
      }