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 |
+-------------------+-----------------+------------------------------------------------------------------+

下面解释一下每个字段的含义:

  1. signerId length,表示 signerId 长度,占用 4 字节,小端格式。这个例子中为 10000000,换算为十进制就是 16,即 signerId 为 16 字节长。
  2. signerId,表示签名者的帐户名。这个例子中就是 my-first.testnet 的 16 进制编码。
  3. signerPublicKey type,表示类型,0 表示 ed25519。
  4. signerPublicKey,表示使用哪个公钥进行验证。这个例子中为 6313c35aa7c79eac216ab54288a18b0ad4f8301ab04a025a29b587f84e510122,它是帐户 my-first.testnet 唯一绑定的公钥,转换为 base58 编码就是 7fkqm12NvLXSJzF4nxmyPYTmstQ6xNuZ2Pi5UpRAooS1。
  5. nonceForPublicKey,表示公钥 nonce 值,占用 8 字节,小端格式。通过 RPC view_access_key_list 可以查看帐户关联公钥的 nonce 值。这个例子中为 c3ac4a0ed0910000,换算为十进制就是 160322779000003。
  6. receiverId length,表示 receiverId 长度,占用 4 字节,小端格式。这个例子中为 40000000,换算为十进制就是 64,即 receiverId 为 64 字节长。
  7. receiverId,表示接收者的帐户名。这个例子中就是 b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6 的 16 进制编码。
  8. blockHash,近期的区块 Hash。这个例子中为 67543fd97e7c0c7604939361e6aaf99c94a08d9d4c59c3256337df53dd61b3f0,转换为 base58 编码就是 7xMWChNy56yFdt5XcBdQy4inZTToEDR87LqaLfqA2CHD ,它是打包当前交易时的一个近期区块。
  9. actions length,表示 action 个数,占用 4 字节,小端格式。这个例子中为 01000000,表示只有 1 个 action。
  10. action type,这个例子中的 03 表示 Transfer,其它 action 可以参考:https://nomicon.io/RuntimeSpec/Actions
  11. action data,对于 Transfer 来说就是转账数量,占用 16 字节,小端格式。这个例子中为 0000405f67bccdc1c586060000000000,换算为十进制就是 7890000000000000000000000,即转移 7.89 NEAR。
  12. signature type,表示签名类型,0 表示 ed25519。
  13. signature data,即 ed25519 签名数据。节 2.2 会详细介绍它是如何计算出来的。

注: NEAR Tx 的手续费,并没有体现在 Tx 中,而是由节点直接扣除,所以不需要用户指定手续费。目前,用户也无法指定额外小费来加速 Tx 的上链过程。

参考:https://github.com/near/near-api-js/blob/f28796267327fc6905a8c6a7051ff37aaa7bbd06/packages/transactions/src/schema.ts#L224

2.1. 交易 Hash

NEAR 的 Tx Hash 由签名前的数据确定。下面是 Tx Hash 的具体计算实例:

#!/usr/bin/env python

import base58
import hashlib

# 计算 tx hash 时,不需要包含签名数据
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 中的签名数据是一样的。

Author: cig01

Created: <2024-03-31 Sun>

Last updated: <2024-03-31 Sun>

Creator: Emacs 27.1 (Org mode 9.4)