Aptos Raw Tx Breakdown
Table of Contents
1. Aptos 交易
在 Aptos 区块链中,通过 RPC /v1/transactions 可以把签名后的 Tx 提交到链上。本文介绍签名打包 Aptos 交易的细节。
RPC /v1/transactions 支持两种格式提交交易:
- JSON 格式,需要指定 HTTP Header
Content-Type: application/json
- 二进制格式,需要指定 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)是如何计算的呢?
首先,要计算“待签名数据”,其过程如下:
序列化未签名的交易(也就是去掉节 2 中数据的 authenticator 部分),即得到:
9cc107ca00ed1f08c33ebe2ce1d39b213b755e300c608827e0aa7b3d16c6e78f01000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e74087472616e73666572000220ab156a8328d4b4bdb9ab1fba7d8c28f7d62126d01154a3145e10212b5e7c7bae087b00000000000000a0860100000000006400000000000000221326660000000002
把上面结果的前面加上
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 呢?
思路:
- 根据 JSON 重建 Tx,然后采用 BCS 方式编码 Tx(需要 entry_function_payload 中指定函数的 ABI);
- 采用节 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 时:
- 无法确定 "20000000" 应该编码为什么类型(比如编码为
u32
和u64
都可以,但这两者编码出来的数据长度是不一样的); - 无法确定 "0xb26df6e5f2a60248ab61deff98c6c45e0e8f16fdc5fc5e417e4e4d3b447aefc3" 应该编码为
address
呢,还是vector<u8>
(这两者编码出来的数据也不一样)。
为了消除这些歧义,我们可以使用 RPC Get account module 查询 deposit_and_stake_entry
的 ABI 文档,发现这两个参数的类型分别是 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")