Aptos Raw Tx Breakdown

Table of Contents

1. Aptos 交易

在 Aptos 区块链中,通过 RPC /v1/transactions 可以把签名后的 Tx 提交到链上。本文介绍签名打包 Aptos 交易的细节。

RPC /v1/transactions 支持两种格式提交交易:

  1. JSON 格式,需要指定 HTTP Header Content-Type: application/json
  2. 二进制格式,需要指定 HTTP Header Content-Type: application/x.aptos.signed_transaction+bcs

本文采用的是二进制格式。

2. Tx 解析实例:原生币转帐

测试网上交易 0xb2eda34a428fd3f0aaf2c3898c4cefcad251b8533f8e796642199f4e78af8084 的功能是原生币 APT 转帐:

0x9cc107ca00ed1f08c33ebe2ce1d39b213b755e300c608827e0aa7b3d16c6e78f     ---- 0.00000123 APT --->    0xab156a8328d4b4bdb9ab1fba7d8c28f7d62126d01154a3145e10212b5e7c7bae

这个交易是通过节 3.1 中的代码构造,采用二进制格式(即调用 RPC /v1/transactions 时指定 HTTP Header Content-Type: application/x.aptos.signed_transaction+bcs )提交到 Aptos 测试网的。我们把它签名后交易的原始内容转换为 Hex String 形式:

9cc107ca00ed1f08c33ebe2ce1d39b213b755e300c608827e0aa7b3d16c6e78f01000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e74087472616e73666572000220ab156a8328d4b4bdb9ab1fba7d8c28f7d62126d01154a3145e10212b5e7c7bae087b00000000000000a08601000000000064000000000000002213266600000000020020f02b2e4600d68eca9565026b1e9ad528df287a20fe63d98adc5690284e9649d540327e384dacb3679817cf36703150a2e1d10a9f85e635996d91659d72ca57d2e4c18d71032f9ff46802703f5d3b0401b3330b9e318eb7ad18b8099219db302a0a

上面的数据可以分解为下面这种更清晰的表达方式(Aptos 中交易的序列化格式采用的是 Binary Canonical Serialization Library 格式):

 sender                                  9cc107ca00ed1f08c33ebe2ce1d39b213b755e300c608827e0aa7b3d16c6e78f   发送者地址                                                                   
 sequence_number                         0100000000000000                                                   发送者的 sequence_number(相当于以太坊 nonce)                               
         
         
         
         
         
         
         
         
         
         
         
 payload 
         
         
         
         
         
         
         
         
         
         
         
 payload type                  02                                                                 00/01/02/03 分别为 Script/ModuleBundle/EntryFunction/Multisig                
           
           
 module id 
           
           
 address           0000000000000000000000000000000000000000000000000000000000000001   模块地址。这里是 0x1                                                         
      
 name 
      
 length     0d                                                                 模块名称长度                                                                 
 data       6170746f735f6163636f756e74                                         模块名称。这里是字段串 aptos_account 的编码                                  
                  
 function name    
                  
 length     08                                                                 函数名称长度                                                                 
 data       7472616e73666572                                                   函数名称。这里是字段串 transfer 的编码                                       
 type args   length            00                                                                 泛型参数个数。这里不需要,若调用 0x1::aptos_account::transfer_coins 则要设置 
           
           
           
           
 args      
           
           
           
           
 length            02                                                                 参数个数。这里是 2 个参数                                                    
        
 arg 0  
        
 length   20                                                                                                                                              
 data     ab156a8328d4b4bdb9ab1fba7d8c28f7d62126d01154a3145e10212b5e7c7bae   接收者地址                                                                   
        
 arg 1  
        
 length   08                                                                                                                                              
 data     7b00000000000000                                                   转帐数量。这里是数字 123 的编码                                              
 max_gas_amount                          a086010000000000                                                   最大 Gas 限制。这里是数字 100000 的编码                                      
 gas_unit_price                          6400000000000000                                                   Gas 价格。这里是数字 100 的编码                                              
 expiration_timestamp_secs               2213266600000000                                                   过期时间。这里是数字 1713771298 的编码                                       
 chain_id                                02                                                                 01/02 分别表示 mainnet/testnet                                               
                
                
                
                
                
 authenticator  
                
                
                
                
 variant                00                                                                 00/01/02/03 分别为 Ed25519/MultiEd25519/MultiAgent/FeePayer                  
             
 pubkey      
             
 length   20                                                                                                                                              
 data     f02b2e4600d68eca9565026b1e9ad528df287a20fe63d98adc5690284e9649d5   签名者公钥                                                                   
             
             
 signature   
             
 length   40                                                                                                                                              
 data   
        
 327e384dacb3679817cf36703150a2e1d10a9f85e635996d91659d72ca57d2e4 
 c18d71032f9ff46802703f5d3b0401b3330b9e318eb7ad18b8099219db302a0a 
 Ed25519 签名                                                                 
                                                                              

注 1:从上面交易结构中可知,通过 module 0x1::aptos_account 中的 transfer 方法可以实现原生币 APT 的转移。
注 2:AuthenticatorVariant 中 00/01/02/03 分别为 Ed25519/MultiEd25519/MultiAgent/FeePayer,可参考:https://github.com/aptos-labs/aptos-ts-sdk/blob/8a8dcf8d49d142bc2413004d795946ef3b79c2d1/src/types/index.ts#L89

2.1. 签名计算

上一节的例子中,Ed25519 的签名数据(即 327e384dacb3679817cf36703150a2e1d10a9f85e635996d91659d72ca57d2e4c18d71032f9ff46802703f5d3b0401b3330b9e318eb7ad18b8099219db302a0a)是如何计算的呢?

首先,要计算“待签名数据”,其过程如下:

  1. 序列化未签名的交易(也就是去掉节 2 中数据的 authenticator 部分),即得到:

     9cc107ca00ed1f08c33ebe2ce1d39b213b755e300c608827e0aa7b3d16c6e78f01000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e74087472616e73666572000220ab156a8328d4b4bdb9ab1fba7d8c28f7d62126d01154a3145e10212b5e7c7bae087b00000000000000a0860100000000006400000000000000221326660000000002
    
  2. 把上面结果的前面加上 SHA3_256(b'APTOS::RawTransaction') (即加上 b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b193),将得到:

     b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b1939cc107ca00ed1f08c33ebe2ce1d39b213b755e300c608827e0aa7b3d16c6e78f01000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e74087472616e73666572000220ab156a8328d4b4bdb9ab1fba7d8c28f7d62126d01154a3145e10212b5e7c7bae087b00000000000000a0860100000000006400000000000000221326660000000002
    

然后,进行 Ed25519 签名,这一步的细节可以参考节 3.2,最终可以得到结果:

327e384dacb3679817cf36703150a2e1d10a9f85e635996d91659d72ca57d2e4c18d71032f9ff46802703f5d3b0401b3330b9e318eb7ad18b8099219db302a0a

2.2. 从 Hex Tx 计算 Tx Hash

下面 Python 代码演示从 BCS 编码的 Tx 计算 Tx Hash 的规则:

import hashlib

prefix = hashlib.sha3_256(b'APTOS::Transaction').digest()
enum_byte = bytes.fromhex('00')  # 0 means the first item in enum Transaction (UserTransaction), see: https://github.com/aptos-labs/aptos-core/blob/6cdd4c27275f355774e55d346738f346b629289e/types/src/transaction/mod.rs#L1954
tx_data = bytes.fromhex('9cc107ca00ed1f08c33ebe2ce1d39b213b755e300c608827e0aa7b3d16c6e78f01000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e74087472616e73666572000220ab156a8328d4b4bdb9ab1fba7d8c28f7d62126d01154a3145e10212b5e7c7bae087b00000000000000a08601000000000064000000000000002213266600000000020020f02b2e4600d68eca9565026b1e9ad528df287a20fe63d98adc5690284e9649d540327e384dacb3679817cf36703150a2e1d10a9f85e635996d91659d72ca57d2e4c18d71032f9ff46802703f5d3b0401b3330b9e318eb7ad18b8099219db302a0a')

tx_hash = hashlib.sha3_256(prefix + enum_byte + tx_data).hexdigest()
print("tx hash:", tx_hash)  # b2eda34a428fd3f0aaf2c3898c4cefcad251b8533f8e796642199f4e78af8084

2.3. 从 JSON Tx 计算 Tx Hash

当使用 RPC /v1/transactions 时采用 JSON 方式提交 Tx(即指定的 HTTP Header 为 Content-Type: application/json )时,如何在提交这个 JSON 格式的 Tx 之前计算 Tx Hash 呢?

思路:

  1. 根据 JSON 重建 Tx,然后采用 BCS 方式编码 Tx(需要 entry_function_payload 中指定函数的 ABI);
  2. 采用节 2.2 的方式计算 Tx Hash。

但是“根据 JSON 重建 Tx,然后采用 BCS 方式编码 Tx”这个步骤需要 function 字段所关联函数的 ABI 信息。假设 JSON 中的 payload 字段内容如下:

{
    "type": "entry_function_payload",
    "function": "0x111ae3e5bc816a5e63c2da97d0aa3886519e0cd5e4b046659fa35796bd11542a::router::deposit_and_stake_entry",
    "type_arguments": [],
    "arguments": [
        "20000000",
        "0xb26df6e5f2a60248ab61deff98c6c45e0e8f16fdc5fc5e417e4e4d3b447aefc3"
    ]
}

如果不知道函数 deposit_and_stake_entry 的 ABI,那么我们在编码为 BCS 时:

  1. 无法确定 "20000000" 应该编码为什么类型(比如编码为 u32u64 都可以,但这两者编码出来的数据长度是不一样的);
  2. 无法确定 "0xb26df6e5f2a60248ab61deff98c6c45e0e8f16fdc5fc5e417e4e4d3b447aefc3" 应该编码为 address 呢,还是 vector<u8> (这两者编码出来的数据也不一样)。

为了消除这些歧义,我们可以使用 RPC Get account module 查询 deposit_and_stake_entryABI 文档,发现这两个参数的类型分别是 u64/address ,这时才可以无歧义地编码。

下面是使用 RPC Get account module 查询模块 0x111ae3e5bc816a5e63c2da97d0aa3886519e0cd5e4b046659fa35796bd11542a::router 的 ABI 的例子:

$ curl 'https://fullnode.mainnet.aptoslabs.com/v1/accounts/0x111ae3e5bc816a5e63c2da97d0aa3886519e0cd5e4b046659fa35796bd11542a/module/router' | jq '.abi.exposed_functions | .[] | select(.name == "deposit_and_stake_entry")'
{
  "name": "deposit_and_stake_entry",
  "visibility": "public",
  "is_entry": true,
  "is_view": false,
  "generic_type_params": [],
  "params": [
    "&signer",
    "u64",
    "address"
  ],
  "return": []
}

从输出中可见函数 deposit_and_stake_entry 的两个参数类型分别为 u64/address

3. 附录

3.1. 构造转帐交易并提交上链(Python)

下面 Python 代码可以构造并广播节 2 中所演示的交易:

import asyncio
import time

from aptos_sdk.account import Account               # pip install aptos-sdk
from aptos_sdk.async_client import RestClient
from aptos_sdk.bcs import Serializer
from aptos_sdk.transactions import (
    EntryFunction,
    TransactionArgument,
    TransactionPayload,
    RawTransaction,
    SignedTransaction,
)


async def main():
    base_url = 'https://fullnode.testnet.aptoslabs.com/v1'
    rest_client = RestClient(base_url)

    alice = Account.load_key('b7d58a41ffb3fb0cbfb813624c40fd7c5dad993865e809aec7697c0a02061d11')
    bob = Account.load_key('ccfbdef863a7bd27e3a3a7c5a897201b63aa95bfe46f6e44c0d224fc382dbf01')

    sender = alice
    recipient = bob.address()
    amount = 123

    # 下面的所有代码其实用一个函数 rest_client.bcs_transfer(sender, recipient, amount) 就行了,这里仅仅是演示目的

    transaction_arguments = [
        TransactionArgument(recipient, Serializer.struct),
        TransactionArgument(amount, Serializer.u64),
    ]

    payload = EntryFunction.natural(
        module="0x1::aptos_account",
        function="transfer",
        ty_args=[],
        args=transaction_arguments,
    )

    sequence_number = await rest_client.account_sequence_number(sender.address())
    chain_id = await rest_client.chain_id()  # 1/2: mainnet/testnet

    raw_transaction = RawTransaction(
        sender.address(),
        sequence_number,
        TransactionPayload(payload),
        rest_client.client_config.max_gas_amount,
        rest_client.client_config.gas_unit_price,
        int(time.time()) + rest_client.client_config.expiration_ttl,
        chain_id,
        )

    authenticator = raw_transaction.sign(sender.private_key)
    signed_transaction = SignedTransaction(raw_transaction, authenticator)

    print("signed_transaction (hex)", signed_transaction.bytes().hex())  # 9cc107ca00ed1f08c33ebe2ce1d39b213b755e300c608827e0aa7b3d16c6e78f01000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e74087472616e73666572000220ab156a8328d4b4bdb9ab1fba7d8c28f7d62126d01154a3145e10212b5e7c7bae087b00000000000000a08601000000000064000000000000002213266600000000020020f02b2e4600d68eca9565026b1e9ad528df287a20fe63d98adc5690284e9649d540327e384dacb3679817cf36703150a2e1d10a9f85e635996d91659d72ca57d2e4c18d71032f9ff46802703f5d3b0401b3330b9e318eb7ad18b8099219db302a0a

    # 提交上链
    headers = {"Content-Type": "application/x.aptos.signed_transaction+bcs"}
    response = await rest_client.client.post(
        f"{base_url}/transactions",
        headers=headers,
        content=signed_transaction.bytes(),
    )
    print("node response", response.text)
    tx_hash = response.json()["hash"]
    print("tx_hash", tx_hash)

    await rest_client.close()


if __name__ == "__main__":
    asyncio.run(main())

执行下面程序,得到下面输出:

signed_transaction (hex) 9cc107ca00ed1f08c33ebe2ce1d39b213b755e300c608827e0aa7b3d16c6e78f01000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e74087472616e73666572000220ab156a8328d4b4bdb9ab1fba7d8c28f7d62126d01154a3145e10212b5e7c7bae087b00000000000000a08601000000000064000000000000002213266600000000020020f02b2e4600d68eca9565026b1e9ad528df287a20fe63d98adc5690284e9649d540327e384dacb3679817cf36703150a2e1d10a9f85e635996d91659d72ca57d2e4c18d71032f9ff46802703f5d3b0401b3330b9e318eb7ad18b8099219db302a0a
node response {"hash":"0xb2eda34a428fd3f0aaf2c3898c4cefcad251b8533f8e796642199f4e78af8084","sender":"0x9cc107ca00ed1f08c33ebe2ce1d39b213b755e300c608827e0aa7b3d16c6e78f","sequence_number":"1","max_gas_amount":"100000","gas_unit_price":"100","expiration_timestamp_secs":"1713771298","payload":{"function":"0x1::aptos_account::transfer","type_arguments":[],"arguments":["0xab156a8328d4b4bdb9ab1fba7d8c28f7d62126d01154a3145e10212b5e7c7bae","123"],"type":"entry_function_payload"},"signature":{"public_key":"0xf02b2e4600d68eca9565026b1e9ad528df287a20fe63d98adc5690284e9649d5","signature":"0x327e384dacb3679817cf36703150a2e1d10a9f85e635996d91659d72ca57d2e4c18d71032f9ff46802703f5d3b0401b3330b9e318eb7ad18b8099219db302a0a","type":"ed25519_signature"}}
tx_hash 0xb2eda34a428fd3f0aaf2c3898c4cefcad251b8533f8e796642199f4e78af8084

注:由于交易中有时间戳,而且发送者的 sequence_number 会增加,所以重复运行上面代码得到的结果会不一样。

3.2. 计算签名(Python)

下面是计算节 2 中所演示的交易的 Ed25519 签名数据:

import hashlib
import nacl.encoding
import nacl.signing

# 指定私钥
private_key = bytes.fromhex('b7d58a41ffb3fb0cbfb813624c40fd7c5dad993865e809aec7697c0a02061d11')

# 使用指定的私钥生成签名密钥
signing_key = nacl.signing.SigningKey(private_key, encoder=nacl.encoding.RawEncoder)

# 生成验证密钥
verify_key = signing_key.verify_key
print("public key:", verify_key.encode().hex())

# 要签名的消息
prepend_data = hashlib.sha3_256(b'APTOS::RawTransaction').hexdigest()  # b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b193
data = bytes.fromhex(prepend_data + '9cc107ca00ed1f08c33ebe2ce1d39b213b755e300c608827e0aa7b3d16c6e78f01000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e74087472616e73666572000220ab156a8328d4b4bdb9ab1fba7d8c28f7d62126d01154a3145e10212b5e7c7bae087b00000000000000a0860100000000006400000000000000221326660000000002')

# 对 data 进行签名
signature = signing_key.sign(data).signature
print("signature:", signature.hex())     # 327e384dacb3679817cf36703150a2e1d10a9f85e635996d91659d72ca57d2e4c18d71032f9ff46802703f5d3b0401b3330b9e318eb7ad18b8099219db302a0a

# 验证签名
try:
    verify_key.verify(data, signature)
    print("Signature verified")
except nacl.exceptions.BadSignatureError:
    print("Signature verification failed")

Author: cig01

Created: <2024-04-21 Sun>

Last updated: <2024-09-08 Sun>

Creator: Emacs 27.1 (Org mode 9.4)