Polkadot Raw Tx Breakdown
Table of Contents
1. Polkadot 交易
在 Polkadot 区块链中,通过 RPC author_submitExtrinsic 可以把签名后的 Tx 提交到链上。
2. Tx 解析实例
下面以 Polkadot 的 Westend 测试网(注:这个测试网原生币的精度为 12,和 Polkadot 主网原生币 DOT 的精度 10 不一样)上 Tx 0x1f7eb2a1348e0e0c68cc090b111b51e68b19f474325b471a60da695ec3d080ee 为例介绍一下这个 Tx 的细节。
这个交易的功能是测试网上原生币 WND 的转帐:
5GEvhpdiNfs5H6LHNsC3BvX8NuKQRpyUeNGDLw7CcQZZYjZT ---- 1.23123456789 WND ---> 5GP8LUUmhpFM6bZHzJhnixh82QVTJHoxiMhUz3J9cRcncp7E
这个 Tx 是通过 RPC author_submitExtrinsic 提交上链的,具体参数如下:
$ curl https://westend-rpc.polkadot.io -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "author_submitExtrinsic", "params": [ "0x51028400b8bca2e47462d3f076fa54cb9cf4e3de51702a92b8884d6a4c5481d2f28a9b37013850bfab86f091df7fac6e74edfb56ee2b1c3516c0802d2406b71dba8a24ed03bf746b89470c65d8a7bec47ac78073936f8b133b1abc16cecc081beb2b1c8d80d50200ca3ff102040300befdc191f6ddc7132bf03d93011e0c756ead1fc2c25fd35a15144e9b0cb80f760bd28e4cab1e01" ] } ' {"jsonrpc":"2.0","result":"0x1f7eb2a1348e0e0c68cc090b111b51e68b19f474325b471a60da695ec3d080ee","id":1}
上面命令中提到的十六进制 Tx 数据可以分解为下面这种更清晰的表达方式(Polkadot 中交易的序列化格式采用的是 SCALE 编码):
length | 5102 | 长度。148 的 Compact Integer 编码,表示 length 后面还有 148 字节数据 | ||
tx version | 84 | 首个 bit 位为 1 表示 signed tx,后 7 bits 表示 Tx 版本号,这里是版本 4 | ||
sender account |
acct type | 00 | 00 表示 MultiAddress 枚举值的首个选项,即 account ID (pubkey) | |
pubkey | b8bca2e47462d3f076fa54cb9cf4e3de51702a92b8884d6a4c5481d2f28a9b37 | 发送者公钥 | ||
signature |
sign type | 01 | 签名类型。00/01 分别表示 Ed25519/sr25519,这里是 sr25519 | |
data |
3850bfab86f091df7fac6e74edfb56ee2b1c3516c0802d2406b71dba8a24ed03 bf746b89470c65d8a7bec47ac78073936f8b133b1abc16cecc081beb2b1c8d80 |
签名数据 |
||
extra data |
era | d502 | 过期时间。编码了 blockNumber 20804589 和 eraPeriod 64 | |
nonce | 00 | nonce 值。这里是 0 的 Compact Integer 编码,地址发出的首个交易 | ||
tip | ca3ff102 | 手续费小费,这里是 12341234 的 Compact Integer 编码 | ||
call data |
pallet index | 04 | pallet 索引,这里 4 表示 pallet balances(索引号不固定,其它链会不同) | |
function index | 03 | pallet 中 function(即 call)的索引,这里 3 表示 transfer_keep_alive | ||
arg0 |
acct type | 00 | 表示 MultiAddress 枚举值的首个选项,即 account ID (pubkey) | |
pubkey | befdc191f6ddc7132bf03d93011e0c756ead1fc2c25fd35a15144e9b0cb80f76 | 接收者公钥,对应地址为 5GP8LUUmhpFM6bZHzJhnixh82QVTJHoxiMhUz3J9cRcncp7E | ||
arg1 | value | 0bd28e4cab1e01 | 转帐金额,这里是 1231234567890 的 Compact Integer 编码 |
注:
- 帐户类型除了公钥外,还有其它类型,可以参考 MultiAddress 实现。
- 关于 era 的具体编码可以参考 wallet core 中 encodeEra。
- 上面交易使用了模块 pallet_balances 中的方法 transfer_keep_alive 实现原生币转移(Polkadot 中当帐户余额小于 1 DOT 时会删除帐户,这里方法名 transfer_keep_alive 中的 keep alive 表示确保帐户不会因为这次转账而被删除,如果转移后发现余额小于 1 DOT 则会禁止转移)。也可以使用 balances 模块中的方法 transfer_allow_death(这个方式允许转移后帐户余额小于 1 DOT 而导致帐户被删除)实现原生币转移,或者使用 balances 模块中的方法 transfer_all 实现全部原生币的转移。
- 同一模块(比如 pallet balances)的索引值在不同的链上可能不同,比如 Polkadot 主网中模块 pallet balances 的索引值是 5(而不是 Westend 测试网的 4)。模块具体的索引值可以通过 Substrate API Sidecar 中 runtime/metadata 接口进行查询。
- 关于 Polkadot Tx 的格式,可以参考:https://docs.substrate.io/reference/transaction-format/ 。
2.1. 签名计算
上一节的例子中,sr25519 的签名数据(即 3850bfab86f091df7fac6e74edfb56ee2b1c3516c0802d2406b71dba8a24ed03bf746b89470c65d8a7bec47ac78073936f8b133b1abc16cecc081beb2b1c8d80)是如何计算的呢?
首先,要计算“待签名数据”,它由下面数据组成(需要注意的是下面数据并不是一个固定不变的结构, 随着 specVersion 的不同,待签名数据的格式可能不同 ):
040300befdc191f6ddc7132bf03d93011e0c756ead1fc2c25fd35a15144e9b0cb80f760bd28e4cab1e01 // call data d50200ca3ff102 // extra data(即 era / nonce / tip) 386d0f00 // specVersion,占用 4 字节。这里是十进制数 1011000 的小端编码 19000000 // transactionVersion,占用 4 字节。这里是十进制数 25 的小端编码 e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e // genesisHash c3253dd4dd4f141aeafb171494e0d742cb36a6c5a3bda7186c1d989111aa1bdb // blockHash
在节 3.2 和节 3.3 中介绍了如何从 RPC 获得 nonce/specVersion/transactionVersion/genesisHash/blockHash。把上面的数据合并在一起,就得到待签名数据:
040300befdc191f6ddc7132bf03d93011e0c756ead1fc2c25fd35a15144e9b0cb80f760bd28e4cab1e01d50200ca3ff102386d0f0019000000e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423ec3253dd4dd4f141aeafb171494e0d742cb36a6c5a3bda7186c1d989111aa1bdb
如果待签名数据长度多于 256 字节,则要把待签名数据进行 blake2b_256 哈希运算,其结果(32 字节数据)作为最终的待签名数据(参考 polkadot-sdk 中的源码)。我们这个例子中,待签名数据长度为 122 字节(小于 256 字节),所以不用进行 blake2b_256 处理,直接进行签名就行。
最后,我们对上面数据进行 sr25519 签名(细节可参考节 3.4),即可得到签名数据:
3850bfab86f091df7fac6e74edfb56ee2b1c3516c0802d2406b71dba8a24ed03bf746b89470c65d8a7bec47ac78073936f8b133b1abc16cecc081beb2b1c8d80
2.2. Tx Hash 计算
Polkadot 的 Tx Hash 是签名后 Tx 的 blake2b_256 哈希。下面是 Tx Hash 的具体计算实例:
#!/usr/bin/env python import hashlib data = bytes.fromhex('51028400b8bca2e47462d3f076fa54cb9cf4e3de51702a92b8884d6a4c5481d2f28a9b37013850bfab86f091df7fac6e74edfb56ee2b1c3516c0802d2406b71dba8a24ed03bf746b89470c65d8a7bec47ac78073936f8b133b1abc16cecc081beb2b1c8d80d50200ca3ff102040300befdc191f6ddc7132bf03d93011e0c756ead1fc2c25fd35a15144e9b0cb80f760bd28e4cab1e01') tx_hash = hashlib.blake2b(data, digest_size=32).hexdigest() print("tx hash:", tx_hash) # 1f7eb2a1348e0e0c68cc090b111b51e68b19f474325b471a60da695ec3d080ee
3. 附录
3.1. Compact Integer
SCALE 中定义了 Compact Integer,它是一种变长编码,交易中很多整数采用了这种编码。
Compact Integer 有 4 种 mode,它是根据 two least significant bit 来区分 mode,具体来说:
- 当 two least significant bit 是 00 时,表示 single-byte mode。剩下的 6 个 bits 用于用小端格式编码数字,范围是 0 到 63 的整数。
- 当 two least significant bit 是 01 时,表示 two-byte mode。剩下的 6 个 bits ,以及后面的 1 byte 用于用小端格式编码数字,范围是 64 到
的整数。 - 当 two least significant bit 是 10 时,表示 four-byte mode。剩下的 6 个 bits ,以及后面的 3 bytes 用于用小端格式编码数字,范围是
到 的整数。 - 当 two least significant bit 是 11 时,表示 big-integer mode。剩下的 6 个 bits 用于说明比 four-byte mode 要多多少个字节。
表 1 是 Compact Integer 的几个实例。
Compact Integer 实例 | 所编码数字的十进制表示 |
---|---|
0xa8 | 42 |
0x5102 | 148 |
0xca3ff102 | 12341234 |
下面用例子介绍一下 Compact Integer 的具体解码过程:
0xa8 -> 10101000 # 0xa8 的二进制表示(由于 two least significant bit 是 00,所以知道它是 single-byte mode) -> 101010 # 右移两个比特位(去掉了最低两位,去掉的 00 仅仅是 mode 指示位) -> 0x2a # 对应十六进制 -> 42 # 对应十进制 0x5102 -> 01010001 00000010 # 0x5102 的二进制表示(由于 two least significant bit 是 01,所以知道它是 two-byte mode) -> 00000010 01010001 # 交换字节序,Compact Integer 是小端编码 -> 000000 10010100 # 右移两个比特位(去掉了最低两位,去掉的 01 仅仅是 mode 指示位) -> 0x94 # 对应十六进制 -> 148 # 对应十进制 0xca3ff102 -> 11001010 00111111 11110001 00000010 # 0xca3ff102 的二进制表示(由于 two least significant bit 是 10,所以知道它是 four-byte mode) -> 00000010 11110001 00111111 11001010 # 交换字节序,Compact Integer 是小端编码 -> 000000 10111100 01001111 11110010 # 右移两个比特位(去掉了最低两位,去掉的 10 仅仅是 mode 指示位) -> 0xbc4ff2 # 对应十六进制 -> 12341234 # 对应十进制
3.2. 获得地址的 nonce 值
假设地址发出过 10 笔交易,而且还有 2 笔交易正在打包中。这时有两种类型的 nonce 值:
- 只考虑已经被打包上链的交易,这种情况下 nonce 值为 10;
- 考虑 pending tx pool 中的交易,这种情况下 nonce 值为 12。
对于第 1 种 nonce 值(不考虑 pending tx pool 中的交易),可以通过 Polkadot.js 中的方法 api.query.system.account 获得,如果不想使用这个 JS 方法,可以参考:https://substrate.stackexchange.com/questions/3254/is-it-possible-to-fetch-balance-and-nonce-without-using-polkadot-js
对于第 2 种 nonce 值(考虑 pending tx pool 中的交易),可以通过 RPC system_accountNextIndex 获得,比如:
$ curl https://westend-rpc.polkadot.io -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "system_accountNextIndex", "params": ["5GEvhpdiNfs5H6LHNsC3BvX8NuKQRpyUeNGDLw7CcQZZYjZT"] } '
3.3. 获得 specVersion/blockHash 等
节 2.1 中提到在构造“待签名数据”时,需要 specVersion/transactionVersion/genesisHash/blockHash,这些数据可以从节点 RPC 中获得。
下面是通过 RPC state_getRuntimeVersion 获得 specVersion/transactionVersion 的例子:
$ curl https://westend-rpc.polkadot.io -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "state_getRuntimeVersion", "params": [] } ' {"jsonrpc":"2.0","result":{"specName":"westend","implName":"parity-westend","authoringVersion":2,"specVersion":1011000,"implVersion":0,"apis":[["0xdf6acb689907609b",5],["0x37e397fc7c91f5e4",2],["0x40fe3ad401f8959a",6],["0xd2bc9897eed08f15",3],["0xf78b278be53f454c",2],["0xaf2c0297a23e6d3d",11],["0x49eaaf1b548a0cb0",3],["0x91d5df18b0d2cf58",2],["0x2a5e924655399e60",1],["0xed99c5acb25eedf5",3],["0xcbca25e39f142387",2],["0x687ad44ad37f03c2",1],["0xab3c0572291feb8b",1],["0xbc9d89904f5b923f",1],["0x37c8bb1350a9a2a8",4],["0xf3ff14d5ab527059",3],["0x6ff52ee858e6c5bd",1],["0x17a6bc0d0062aeb3",1],["0x18ef58a3b67ba770",1],["0xfbc577b9d747efd6",1]],"transactionVersion":25,"stateVersion":1},"id":1}
下面是通过 RPC chain_getBlockHash 获得 blockHash 的例子:
$ curl https://westend-rpc.polkadot.io -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "chain_getBlockHash", "params": [] } ' {"jsonrpc":"2.0","result":"0xc3253dd4dd4f141aeafb171494e0d742cb36a6c5a3bda7186c1d989111aa1bdb","id":1}
下面是通过 RPC chain_getBlockHash(指定参数为 0)获得 genesisHash 的例子:
$ curl https://westend-rpc.polkadot.io -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "chain_getBlockHash", "params": [0] } ' {"jsonrpc":"2.0","result":"0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e","id":1}
3.4. 计算签名(Javascript)
下面 Javascript 代码可以计算节 2 中所演示的交易的 sr25519 签名数据:
import { cryptoWaitReady, mnemonicToMiniSecret, sr25519Sign, sr25519Verify, sr25519PairFromSeed } from '@polkadot/util-crypto' // "^12.6.2" import { hexToU8a, u8aToHex } from "@polkadot/util" // "^12.6.2" async function main() { // Wait for the promise to resolve async WASM await cryptoWaitReady(); // Create a sr25519Pair from a mnemonic const MNEMONIC = 'left salmon glimpse surface chicken open jump refuse boil slot age kiss'; const miniSecret = mnemonicToMiniSecret(MNEMONIC) // bip32 的 master private key 0xc96bbc32f5671903ad3c4bf6a066abe56d3064dcfaebf2757a6d0fdd5c3b669f const keyPair = sr25519PairFromSeed(miniSecret) // const prikey = keyPair.secretKey // 上面 miniSecret 进行 SHA512(模仿 ed2519)后的结果(64 字节)0xd8d4b22c490cb82c76522cf379b50f4d31e109a37f1d7f56488bf02ba0bd674bd9db3fafe701aeb616fe98b6c72ffbed46fdf1a0861947b4fbc51ad0b3693db3 const pubkey = keyPair.publicKey // 注:用下面这些更高层的函数也可以得到相同密钥 // const keyring = new Keyring({ type: 'sr25519', ss58Format: 42 }); // import { Keyring } from "@polkadot/keyring" // const keyringPair = keyring.createFromUri(MNEMONIC); // const pubkey = keyringPair.publicKey console.log("pubkey", u8aToHex(pubkey)) // 0xb8bca2e47462d3f076fa54cb9cf4e3de51702a92b8884d6a4c5481d2f28a9b37 // Sign const msg = hexToU8a("040300befdc191f6ddc7132bf03d93011e0c756ead1fc2c25fd35a15144e9b0cb80f760bd28e4cab1e01d50200ca3ff102386d0f0019000000e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423ec3253dd4dd4f141aeafb171494e0d742cb36a6c5a3bda7186c1d989111aa1bdb") const sig = sr25519Sign(msg, keyPair) // sr25519 不是确定性的签名算法,所以多次运行时,所打印的签名结果会不同 console.log("sig", u8aToHex(sig)) // 0x3850bfab86f091df7fac6e74edfb56ee2b1c3516c0802d2406b71dba8a24ed03bf746b89470c65d8a7bec47ac78073936f8b133b1abc16cecc081beb2b1c8d80 // Verify const result = sr25519Verify(msg, sig, pubkey); console.log(result) } main().catch((error) => { console.error(error); process.exit(1); });
注意:sr25519 不是确定性的签名算法(这点和 Ed25519 不同,Ed25519 是确定性的签名算法),所以多次运行时,打印的签名结果会不同。
3.5. 构造转帐交易
通过 polkadot.js 可以构造转帐交易,参考:https://polkadot.js.org/docs/api/examples/promise/make-transfer ,这是一套 high level 的 API。如果想使用更加 low level 的方式来构造交易,可以使用 txwrapper,参考:https://github.com/paritytech/txwrapper-core/tree/main/packages/txwrapper-examples/polkadot 。