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 编码                  

注:

  1. 帐户类型除了公钥外,还有其它类型,可以参考 MultiAddress 实现。
  2. 关于 era 的具体编码可以参考 wallet core 中 encodeEra
  3. 上面交易使用了模块 pallet_balances 中的方法 transfer_keep_alive 实现原生币转移(Polkadot 中当帐户余额小于 1 DOT 时会删除帐户,这里方法名 transfer_keep_alive 中的 keep alive 表示确保帐户不会因为这次转账而被删除,如果转移后发现余额小于 1 DOT 则会禁止转移)。也可以使用 balances 模块中的方法 transfer_allow_death(这个方式允许转移后帐户余额小于 1 DOT 而导致帐户被删除)实现原生币转移,或者使用 balances 模块中的方法 transfer_all 实现全部原生币的转移。
  4. 同一模块(比如 pallet balances)的索引值在不同的链上可能不同,比如 Polkadot 主网中模块 pallet balances 的索引值是 5(而不是 Westend 测试网的 4)。模块具体的索引值可以通过 Substrate API Sidecar 中 runtime/metadata 接口进行查询。
  5. 关于 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,具体来说:

  1. 当 two least significant bit 是 00 时,表示 single-byte mode。剩下的 6 个 bits 用于用小端格式编码数字,范围是 0 到 63 的整数。
  2. 当 two least significant bit 是 01 时,表示 two-byte mode。剩下的 6 个 bits ,以及后面的 1 byte 用于用小端格式编码数字,范围是 64 到 2141 的整数。
  3. 当 two least significant bit 是 10 时,表示 four-byte mode。剩下的 6 个 bits ,以及后面的 3 bytes 用于用小端格式编码数字,范围是 2142301 的整数。
  4. 当 two least significant bit 是 11 时,表示 big-integer mode。剩下的 6 个 bits 用于说明比 four-byte mode 要多多少个字节。

1 是 Compact Integer 的几个实例。

Table 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 值:

  1. 只考虑已经被打包上链的交易,这种情况下 nonce 值为 10;
  2. 考虑 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

Author: cig01

Created: <2024-05-05 Sun>

Last updated: <2024-05-16 Thu>

Creator: Emacs 27.1 (Org mode 9.4)