Aptos - Aptos Raw Tx Breakdown

  • cig01
  • 发布于 2024-04-22 21:47
  • 阅读 6

本文详细介绍了 Aptos 区块链中交易的构成,特别是如何构造和解析 Aptos 原生币转账交易的二进制格式,内容包括交易的结构、签名计算,以及如何从 JSON 交易计算交易哈希,并提供了使用 Python 构造和提交交易的示例代码。

Created: <2024-04-21 Sun>

Last updated: <2024-09-08 Sun>

Aptos Raw Tx Breakdown

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 <br>  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
  1. 把上面结果的前面加上 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&lt;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 ,有了这个信息,我们就可以正确地编码它的参数,即 "20000000" 和 "0xb26df6e5f2a60248ab61deff98c6c45e0e8f16fdc5fc5e417e4e4d3b447aefc3" 了。

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          # pip install pynacl
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")
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
cig01
cig01
https://aandds.com/