NEAR Raw Tx Breakdown
Table of Contents
1. NEAR 交易
在 NEAR 区块链中,通过 RPC send-tx 可以把签名后的 Tx(采用 base64 编码)提交到链上。本文介绍签名打包 NEAR Tx 的细节。
1.1. 演示帐户
本文介绍测试网上 Tx D5v7dwKQ8ohe6TB5zUEoUvXPMVUXhqz4EBBGKFfBseh8 的签名打包过程。这个 Tx 的功能是从帐户 my-first.testnet 转移 7.89 NEAR 到帐户 b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6 中,即:
my-first.testnet ------- 7.89 NEAR -------> b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6
其中发送者帐户 my-first.testnet 是通过下面命令创建的:
$ near create-account my-first.testnet --useFaucet --networkId testnet # 创建帐户 my-first.testnet,并从内置 faucet 中领取测试币 Storing credentials for account: my-first.testnet (network: testnet) Saving key to '~/.near-credentials/testnet/my-first.testnet.json' $ cat ~/.near-credentials/testnet/my-first.testnet.json # 查看保存的密钥(base58 编码) {"account_id":"my-first.testnet","public_key":"ed25519:7fkqm12NvLXSJzF4nxmyPYTmstQ6xNuZ2Pi5UpRAooS1","private_key":"ed25519:4ZHyGAY8XEr3Z3JxBTZsp17b8HHhjnC5S3T82t3Kgr2da3UCg58CsovZk6g5iiiVp2gyP8xDs9WX5TFkf2kJtQfb"}
2. Tx 解析实例
Tx D5v7dwKQ8ohe6TB5zUEoUvXPMVUXhqz4EBBGKFfBseh8 是通过 RPC send-tx 提交上链的,具体的参数如下:
$ curl -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": "dontcare", "method": "send_tx", "params": { "signed_tx_base64": "EAAAAG15LWZpcnN0LnRlc3RuZXQAYxPDWqfHnqwharVCiKGLCtT4MBqwSgJaKbWH+E5RASLDrEoO0JEAAEAAAABiODE2MGIyMmJhNjNkZjIxMTVkYTBhN2MyNjY3OTA2OWEzNGM2NjMyOWMzM2VkNzNlYzBlYjcyNDBmNDcwNmE2Z1Q/2X58DHYEk5Nh5qr5nJSgjZ1MWcMlYzffU91hs/ABAAAAAwAAQF9nvM3BxYYGAAAAAAAAUXUlRGqCiBarySg8SQLTp5eVqk8mIwZsRx4URH9WJLqni8UpvaEyWHMeNpK5oRxjGhVz3sqzzrpmK6qZweMtAw==", "wait_until": "INCLUDED_FINAL" } }' https://rpc.testnet.near.org # 通过 RPC send_tx 发送签名后的 Tx 到 NEAR 网络中 {"jsonrpc":"2.0","result":{"final_execution_status":"EXECUTED","receipts_outcome":[{"block_hash":"4h9StkZejnSyZgVGyfuA1kme8mViYqxE2R7uXcscnoWv","id":"9kto9vwtqDStVU19GoyLws2xFw1XzyKoyjuRZhw2QALZ","outcome":{"executor_id":"b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6","gas_burnt":4174947687500,"logs":[],"metadata":{"gas_profile":[],"version":3},"receipt_ids":["Hqh6Tm7KFbe1XaQ7ijEvx5Q1mMP3yNRW3put2gmSRD1q"],"status":{"SuccessValue":""},"tokens_burnt":"417494768750000000000"},"proof":[{"direction":"Right","hash":"5DVZcJQDQwhvqoNyHYmV1uEy79iSG1FAKpmWXopPyBPQ"},{"direction":"Right","hash":"DrayNokNnMbctgsiF1WUC8cPDyMA9fRovso8aSMM5mS7"},{"direction":"Right","hash":"E52herC8UNMkmv4vw51aDRGiG2JiYKiP5GJ9ngokLECQ"},{"direction":"Left","hash":"7TEvXJFkh5sGMoDaEJpVTdVYCWNHoFoAiQwy9oXuNe5q"},{"direction":"Left","hash":"9EaUpZw5ij9zyZcMAyppCyxjKkWmpcnii9xeLSSwkcAy"},{"direction":"Left","hash":"JDCTNZsqRavuQ3DgsEHk9fs6KirC1SJio8z6HJPb9X9n"}]},{"block_hash":"5e3tmgcy7DfJCCVQw8PRPa2w3TcLM89QG6FTSj7H5GU7","id":"Hqh6Tm7KFbe1XaQ7ijEvx5Q1mMP3yNRW3put2gmSRD1q","outcome":{"executor_id":"my-first.testnet","gas_burnt":223182562500,"logs":[],"metadata":{"gas_profile":[],"version":3},"receipt_ids":[],"status":{"SuccessValue":""},"tokens_burnt":"0"},"proof":[{"direction":"Right","hash":"2uoJEJ67VcZkpNAaibtcsNfLxXdJsqs1YFFmC7nHBCrq"},{"direction":"Right","hash":"F4bvNz2YDsw5xYSLEc41iLUvWjAejne7YGAeoF1mEynV"},{"direction":"Left","hash":"E3dd1NRoZYLYhzMaNaS9Nz2Rxp8RhDixzaV3CSTicmHU"},{"direction":"Left","hash":"9zwGbpR6aVJRi51ZeRfavx4WvDiN6nQAM5T1fQyNfibc"},{"direction":"Right","hash":"6AaDoefwttHgMd4TtNoUem7dqXB8eyS2NBhYmen9NeUa"},{"direction":"Left","hash":"EikMqwiBQn4F3qm9Z9Wyd2t4ZBmSvzNjyvDHrNmfPYRo"}]}],"status":{"SuccessValue":""},"transaction":{"actions":[{"Transfer":{"deposit":"7890000000000000000000000"}}],"hash":"D5v7dwKQ8ohe6TB5zUEoUvXPMVUXhqz4EBBGKFfBseh8","nonce":160322779000003,"public_key":"ed25519:7fkqm12NvLXSJzF4nxmyPYTmstQ6xNuZ2Pi5UpRAooS1","receiver_id":"b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6","signature":"ed25519:2dTc5TTnwGDUi4zy6nDjsBLTAuPZND4CEBZkg33n9J2AiY6eBkJ3HpTvhVHuVPasrjYeqNNQZdHe3xT3GFua18VY","signer_id":"my-first.testnet"},"transaction_outcome":{"block_hash":"8TsRKEjcuMzDnBb41UgNCZwY9vnk92CY3dt1UjMY1ENt","id":"D5v7dwKQ8ohe6TB5zUEoUvXPMVUXhqz4EBBGKFfBseh8","outcome":{"executor_id":"my-first.testnet","gas_burnt":4174947687500,"logs":[],"metadata":{"gas_profile":null,"version":1},"receipt_ids":["9kto9vwtqDStVU19GoyLws2xFw1XzyKoyjuRZhw2QALZ"],"status":{"SuccessReceiptId":"9kto9vwtqDStVU19GoyLws2xFw1XzyKoyjuRZhw2QALZ"},"tokens_burnt":"417494768750000000000"},"proof":[{"direction":"Right","hash":"Ghv3XKxNxAwDu5b6JKuFgFdxogE39xonE3KxPbY51TuW"},{"direction":"Right","hash":"5yqq7BguxPT1kBJ5Z7n742Zo83cdBkxHw3awJHMVK8g3"},{"direction":"Left","hash":"59Wt6onoFjt6ZF7RXyk3XsaoS8NR2ReJWzxqjiAgYvCF"},{"direction":"Right","hash":"6sb7ieHGrM8uo4J2Hi4ZjDsZ7PotwcA8gwLnhCWqyH3i"},{"direction":"Left","hash":"7sGMG7hiRiVGi6kg61tXwiADzZzLX7eomPirAzycK41Y"},{"direction":"Right","hash":"Gt6CNKSHBhFT1Kc29Kgex4bfagSK5za3z4t7yCYGzMUU"}]}},"id":"dontcare"}
为了便于分析,我们把 signed_tx_base64 字段的内容(即 base64 编码的 Tx)转换为 Hex String 形式:
100000006d792d66697273742e746573746e6574006313c35aa7c79eac216ab54288a18b0ad4f8301ab04a025a29b587f84e510122c3ac4a0ed0910000400000006238313630623232626136336466323131356461306137633236363739303639613334633636333239633333656437336563306562373234306634373036613667543fd97e7c0c7604939361e6aaf99c94a08d9d4c59c3256337df53dd61b3f001000000030000405f67bccdc1c58606000000000000517525446a828816abc9283c4902d3a79795aa4f2623066c471e14447f5624baa78bc529bda13258731e3692b9a11c631a1573decab3ceba662baa99c1e32d03
上面的数据可以分解为下面这种更清晰的表达方式(NEAR Tx 使用 Borsh 编码,编码具体规则可参考 https://borsh.io/#pills-specification ):
+-------------------+-----------------+------------------------------------------------------------------+ | | length | 10000000 | | signerId +-----------------+------------------------------------------------------------------+ | | signerId | 6d792d66697273742e746573746e6574 | +-------------------+-----------------+------------------------------------------------------------------+ | | type | 00 | | signerPublicKey +-----------------+------------------------------------------------------------------+ | | signerPublicKey | 6313c35aa7c79eac216ab54288a18b0ad4f8301ab04a025a29b587f84e510122 | +-------------------+-----------------+------------------------------------------------------------------+ | nonceForPublicKey | c3ac4a0ed0910000 | +-------------------+------------------------------------------------------------------------------------+ | | length | 40000000 | | receiverId +-----------------+------------------------------------------------------------------+ | | receiverId | 6238313630623232626136336466323131356461306137633236363739303639 | | | | 6133346336363332396333336564373365633065623732343066343730366136 | +-------------------+-----------------+------------------------------------------------------------------+ | blockHash | 67543fd97e7c0c7604939361e6aaf99c94a08d9d4c59c3256337df53dd61b3f0 | +-------------------+-----------------+------------------------------------------------------------------+ | | length | 01000000 | | +----------+------+------------------------------------------------------------------+ | actions | | type | 03 | | | action 0 +------+------------------------------------------------------------------+ | | | data | 0000405f67bccdc1c586060000000000 | +-------------------+-----------------+------------------------------------------------------------------+ | | type | 00 | | signature +-----------------+------------------------------------------------------------------+ | | signature data | 517525446a828816abc9283c4902d3a79795aa4f2623066c471e14447f5624ba | | | | a78bc529bda13258731e3692b9a11c631a1573decab3ceba662baa99c1e32d03 | +-------------------+-----------------+------------------------------------------------------------------+
下面解释一下每个字段的含义:
- signerId length,表示 signerId 长度,占用 4 字节,小端格式。这个例子中为 10000000,换算为十进制就是 16,即 signerId 为 16 字节长。
- signerId,表示签名者的帐户名。这个例子中就是 my-first.testnet 的 16 进制编码。
- signerPublicKey type,表示类型,0 表示 ed25519。
- signerPublicKey,表示使用哪个公钥进行验证。这个例子中为 6313c35aa7c79eac216ab54288a18b0ad4f8301ab04a025a29b587f84e510122,它是帐户 my-first.testnet 唯一绑定的公钥,转换为 base58 编码就是 7fkqm12NvLXSJzF4nxmyPYTmstQ6xNuZ2Pi5UpRAooS1。
- nonceForPublicKey,表示公钥 nonce 值,占用 8 字节,小端格式。通过 RPC view_access_key_list 可以查看帐户关联公钥的 nonce 值。这个例子中为 c3ac4a0ed0910000,换算为十进制就是 160322779000003。
- receiverId length,表示 receiverId 长度,占用 4 字节,小端格式。这个例子中为 40000000,换算为十进制就是 64,即 receiverId 为 64 字节长。
- receiverId,表示接收者的帐户名。这个例子中就是 b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6 的 16 进制编码。
- blockHash,近期的区块 Hash。这个例子中为 67543fd97e7c0c7604939361e6aaf99c94a08d9d4c59c3256337df53dd61b3f0,转换为 base58 编码就是 7xMWChNy56yFdt5XcBdQy4inZTToEDR87LqaLfqA2CHD ,它是打包当前交易时的一个近期区块。
- actions length,表示 action 个数,占用 4 字节,小端格式。这个例子中为 01000000,表示只有 1 个 action。
- action type,这个例子中的 03 表示 Transfer,其它 action 可以参考:https://nomicon.io/RuntimeSpec/Actions 。
- action data,对于 Transfer 来说就是转账数量,占用 16 字节,小端格式。这个例子中为 0000405f67bccdc1c586060000000000,换算为十进制就是 7890000000000000000000000,即转移 7.89 NEAR。
- signature type,表示签名类型,0/1 分别表示 ed25519/secp256k1。这个例子是 ed25519。
- signature data,即 ed25519 签名数据。节 2.2 会详细介绍它是如何计算出来的。
注: NEAR Tx 的手续费,并没有体现在 Tx 中,而是由节点直接扣除,所以不需要用户指定手续费。目前,用户也无法指定额外小费来加速 Tx 的上链过程。
2.1. Tx Hash 计算
NEAR 的 Tx Hash 由签名前的数据确定。下面是节 2 中 Tx 对应 Tx Hash 的计算实例:
#!/usr/bin/env python import base58 import hashlib # 计算 tx hash 时,不需要包含签名数据(即去掉最后的 00517525446a828816abc9283c4902d3a79795aa4f2623066c471e14447f5624baa78bc529bda13258731e3692b9a11c631a1573decab3ceba662baa99c1e32d03) data = bytes.fromhex('100000006d792d66697273742e746573746e6574006313c35aa7c79eac216ab54288a18b0ad4f8301ab04a025a29b587f84e510122c3ac4a0ed0910000400000006238313630623232626136336466323131356461306137633236363739303639613334633636333239633333656437336563306562373234306634373036613667543fd97e7c0c7604939361e6aaf99c94a08d9d4c59c3256337df53dd61b3f001000000030000405f67bccdc1c586060000000000') pre_hash = hashlib.sha256(data).digest() print("tx hash:", base58.b58encode(pre_hash)) # D5v7dwKQ8ohe6TB5zUEoUvXPMVUXhqz4EBBGKFfBseh8
2.2. 签名
NEAR 的签名数据是对 Tx Hash 的 ed25519 签名,下面是计算实例:
#!/usr/bin/env python import base58 import hashlib import nacl.encoding import nacl.signing # 指定私钥 key = base58.b58decode('4ZHyGAY8XEr3Z3JxBTZsp17b8HHhjnC5S3T82t3Kgr2da3UCg58CsovZk6g5iiiVp2gyP8xDs9WX5TFkf2kJtQfb') private_key = key[:32] # first 32 bytes is private key, last 32 bytes is public key # 使用指定的私钥生成签名密钥 signing_key = nacl.signing.SigningKey(private_key, encoder=nacl.encoding.RawEncoder) # 生成验证密钥 verify_key = signing_key.verify_key print("public key:", verify_key.encode().hex()) # 要签名的消息 data = bytes.fromhex('100000006d792d66697273742e746573746e6574006313c35aa7c79eac216ab54288a18b0ad4f8301ab04a025a29b587f84e510122c3ac4a0ed0910000400000006238313630623232626136336466323131356461306137633236363739303639613334633636333239633333656437336563306562373234306634373036613667543fd97e7c0c7604939361e6aaf99c94a08d9d4c59c3256337df53dd61b3f001000000030000405f67bccdc1c586060000000000') pre_hash = hashlib.sha256(data).digest() print("pre hash:", pre_hash.hex()) print("tx hash:", base58.b58encode(pre_hash)) # D5v7dwKQ8ohe6TB5zUEoUvXPMVUXhqz4EBBGKFfBseh8 # 对 pre_hash 进行签名 signature = signing_key.sign(pre_hash).signature print("signature:", signature.hex()) # 验证签名 try: verify_key.verify(pre_hash, signature) print("Signature verified") except nacl.exceptions.BadSignatureError: print("Signature verification failed")
由于 ed25519 签名是确定性签名,所以上面程序重复运行也总是得到下面输出:
public key: 6313c35aa7c79eac216ab54288a18b0ad4f8301ab04a025a29b587f84e510122 pre hash: b38e9f7ec2d96e0603f969ef4d8b68f751d5cb8154520353aad4bd31457d0afb tx hash: b'D5v7dwKQ8ohe6TB5zUEoUvXPMVUXhqz4EBBGKFfBseh8' signature: 517525446a828816abc9283c4902d3a79795aa4f2623066c471e14447f5624baa78bc529bda13258731e3692b9a11c631a1573decab3ceba662baa99c1e32d03 Signature verified
显然,得到的签名数据和节 2 中的签名数据是一样的。