Polkadot (Blockchain Platform)

Table of Contents

1. 简介

Polkadot 是基于 Substrate 构建的“分片区块链”,它将多个链(可以是异构区块链)连接在一个网络中,允许它们并行处理交易,并能方便地在链之间交换数据。

1.1. 平行链和中继链

Polkadot 网络中的异构区块链分片被称为平行链(Parachains),而主链称为中继链(Relay Chain)。

Polkadot 链本身以及后面介绍的 Kusama 链都属于中继链(Relay Chain)。

平行链可以拥有自己的代币,并针对特定场景优化其功能。为了连接到中继链,平行链可以按需付费或租用一个卡槽以实现连接。

比如,MoonbeamAcalaAstar 等都是 Polkadot 平行链,关于平行链的列表可以参考:https://parachains.info/

1.2. 链之间的消息交换(XCM)

Polkadot 定义了 Cross-Consensus Message Format (XCM) 作为不同链之间进行消息交换时的消息格式。

XCM 仅仅是一种消息格式的定义,并没有规定如何在链之间传递消息。关于如何在链之间传递 XCM 的协议有(如图 1 所示):

  1. Downward Message Passing (DMP)。从中继链传递 XCM 到 Parachains。
  2. Upward Message Passing (UMP)。从 Parachains 传递 XCM 到中继链。
  3. Cross Chain Message Passing (XCMP)。Parachains 之间 XCM 的传递协议,这个协议还在开发过程中。
  4. Horizontally Relay-routed Message Passing (HRMP)。XCMP 的简化版本,HRMP 是目前使用的 Parachains 之间传递 XCM 的协议。

polkadot_xcm.svg

Figure 1: 链之间传递 XCM 的协议

参考:https://paritytech.github.io/polkadot-sdk/book/messaging.html

1.3. Kusama(中继链)

Kusama 和 Polkadot 代码几乎相同,Kusama 与 Polkadot 的一个主要差异在于 Kusama 调节了网络治理参数以允许网络更快的演进。 可以认为 Kusama 是 Polkadot 的一个 pre-production environment,不过 Kusama 并不完全是一个测试网,它的代币是具有真实价值的。

1.4. 原生币 DOT

Polkadot 的原生币是 DOT,它有 10 位精度,如表 1 所示。Kusama 的原生币是 KSM,它有 12 位精度(注:最开始 DOT 的精度也是 12,但后面调整为了 10)。

Table 1: Polkadot 的原生币单位
Unit Decimal Conversion to Planck Conversion to DOT
Planck 0 1 Planck 0.0000000001 DOT
Microdot (uDOT) 4 104 Planck 0.0000010000 DOT
Millidot (mDOT) 7 107 Planck 0.0010000000 DOT
Dot (DOT) 10 1010 Planck 1.0000000000 DOT
Million (MDOT) 16 1016 Planck 1,000,000.00 DOT

参考:https://wiki.polkadot.network/docs/learn-DOT#the-planck-unit

1.5. 两套 RPC

Polkadot 中有两套 RPC:

  1. Polkadot RPC,这个 RPC 接口比较 low level ,它的接收参数和返回数据有时不是很直观,直接调用的话可能需要编码参数和解码返回数据,使用比较麻烦。不过,Polkadot.js 已经在其基础上做了一些封装,Polkadot.js 的使用者可能感受不到它的麻烦。
  2. Substrate API Sidecar,这个 RPC 接口比较 high level ,接收参数以及返回数据都比较友好,使用起来更加方便。

在节 2.4 中介绍分别使用这两套 RPC 完成“查询帐户余额和 nonce 值”的任务。

2. 帐户

2.1. 3 种签名方案

Polkadot 支持 3 种签名方案,如表 2 所示。

Table 2: Polkadot 支持 3 种签名方案
签名 曲线 说明
Schnorr sr25519 Schnorrkel/Ristretto x25519。在 Polkadot 生态钱包中使用广泛,但在其它多链钱包中不够普及
EdDSA ed25519 不支持 sr25519 的多链钱包(如 Ledger)一般会采用这种方案
ECDSA secp256k1 在 Polkadot 生态中使用不多

关于 sr25519 的信息可参考:https://wiki.polkadot.network/docs/learn-cryptography#what-is-sr25519-and-where-did-it-come-from

2.2. 地址格式(SS58)

Polkadot 地址是公钥的 SS58 编码,它是 Base58Check 编码的一个变种。 下面是 SS58 编码的伪代码(完整的 SS58 实现可参考节 7.1):

checksum = blake2b(b'SS58PRE' || version || public_key)[0:2]
address = base58(version || public_key || checksum)

3 是常用的 version 值(也称为 Address Type 或者称为 ss58 Format):

Table 3: Polkadot 生态常用的 Address Type
Version Means 地址前缀
0 (0x00) Polkadot address 地址总是以数字 1 开头,比如 1a1LcBX6hGPKg5aQ6DXZpAHCCzWjckhea4sz3P1PvL3oc4F
2 (0x02) Kusama address 地址总是以大写字母开头,如 C, D, F, G, H, J 等等,比如 D9KrbGKsH1qdntWD9yaKch8VBH6qz1k2TB9DQfcKdX2NVwm
42 (0x2a) Generic substrate address 地址总是以数字 5 开头,比如 5CdiCGvTEuzut954STAXRfL8Lazs3KCZa5LPpkPeqqJXdTHp

注 1:地址是公钥的编码,同一个公钥在不同链上(如果它们采用不同的 Version 值),则会得到不同地址,这些地址之间可以相互转换,这里有个在线转换工具:https://polkadot-address-convertor.netlify.app/
注 2:关于其它的 version 值,可以参考:https://github.com/paritytech/ss58-registry/blob/main/ss58-registry.json

2.3. Existential Deposit

Polkadot 主网中帐户至少需要 1 DOT(这个阈值在平行链中很可能不同的)才能被保留,也就是说如果余额小于 1 DOT,则这个帐户的数据会被删除掉(reaped)。 不过,你并没有失去帐户的控制权,因为如果你再往这个帐户中充值多于 1 DOT,你还可以接着使用这个帐户,只是这个帐户的 nonce 值从重新从 0 开始。

参考:https://wiki.polkadot.network/docs/learn-accounts#existential-deposit-and-reaping

2.4. 查询帐户余额和 nonce 值

下面介绍分别使用节 1.5 中提到的两套 RPC 来完成“查询帐户余额和 nonce 值”的任务:

  1. 调用 Polkadot RPC state_getStorage 可以查询帐户余额和 nonce 值。不过这个 RPC 的参数是 StorageKey 类型,而返回值是 StorageData 类型。待查询地址需要经过一定规则的编码后才能转换为 StorageKey 类型,而结果 StorageData 通过解码后才能得到余额和 nonce 值信息。所以,直接调用 Polkadot RPC 很麻烦,不过这些工作在 Polkadot.js 中已经实现了,也就是说我们直接使用 Polkadot.js 的方法 api.query.system.account 可以方便地查询地址的余额和 nonce 值。
  2. 调用 Substrate API Sidecar 中的 /accounts/{accountId}/balance-info 也可以查询帐户余额和 nonce 值,它使用非常简单。比如:

     $ curl 'https://polkadot-public-sidecar.parity-chains.parity.io/accounts/12nr7GiDrYHzAYT9L8HdeXnMfWcBuYfAXpgfzf3upujeCciz/balance-info'
     {"at":{"hash":"0x6aadecd5c913f15334fe16deff6affd48ad4a0a04eb812bb745a954f730b7736","height":"20785494"},"nonce":"113788","tokenSymbol":"DOT","free":"31609636533176894","reserved":"0","miscFrozen":"miscFrozen does not exist for this runtime","feeFrozen":"feeFrozen does not exist for this runtime","frozen":"0","locks":[]}
    

注意:上面的两种方法得到的 nonce 值都没有考虑 pending tx pool 中的交易,如果想考虑 pending tx pool 中的交易,可以使用 Polkadot RPC system_accountNextIndex

3. 模块(Pallets)

Substrate 中通过模块(也称为 Pallets)来实现和扩展区块链的功能。

比如,原生币转帐可以通过 balances 模块的 transfer_allow_deathtransfer_keep_alivetransfer_all 等方法来实现。

3.1. 查询 Pallet

有两个方式可以查询当前系统中支持哪一些 Pallet:

  1. 通过 Polkadot RPC state_getMetadata,比如:

     $ curl https://rpc.polkadot.io -X POST -H "Content-Type: application/json" -d '
      {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "state_getMetadata",
        "params": []
      }
     '                       # 返回的数据需要解码后才能知道有哪些 Pallet。数据量比较大,这里省略了
    
  2. 通过 Substrate API Sidecar RPC 中的 runtime/metadata 接口,比如:

     $ curl 'https://polkadot-public-sidecar.parity-chains.parity.io/runtime/metadata'   # 直接返回的是解码后的数据。数据量比较大,这里省略了
    

4. 交易(Extrinsics)

在 Substrate 生态中,交易也被称为 Extrinsics。有 3 种类型的交易:

  1. Signed transactions
  2. Unsigned transactions
  3. Inherent transactions

不过,我们接触最多的还是 Signed transactions。

4.1. Tx Hash 不是唯一的

Polkadot 中,Tx Hash 并不能保证唯一性。 比如交易 10218427-210219793-2 ,它们的 Tx Hash 都是 0xf35238b1ef440e2a04576c8264ca8288100091a3d4c71069f0336c72078f366b。

为什么会这样呢?这是因为在节 2.3 中提到过,如果 Polkadot 地址余额少于 1 DOT,则会被删除掉(reaped),地址的 nonce 值也会被删除。这样,如果用户重新激活地址(往这个地址中转移的 DOT 多于 1 即可),这时用户打包交易时使用的 nonce 值又回到 0 了。所以,用户有机会提交两个 Hash 完全相同的交易。表 4 是一个可能的场景。

Table 4: 造成 Tx Hash 不唯一的可能场景(Tx1 和 Tx3 的哈希可能相同)
Tx 功能 A 的 nonce 值 说明
Tx1 A 向 B 转移 5 DOT 0 假设这时 A 余额小于 1 DOT,这会导致 A 被删除掉(reaped)
Tx2 B 向 A 转移 7 DOT(多余 1 就行) N/A 这个操作会再次激活 A。下次 A 再打包交易时,A 的 nonce 又回到了 0
Tx3 A 向 B 转移 5 DOT 0 如果这个交易(Tx3)的所有信息都和 Tx1 一样,那么 Tx1 和 Tx3 的 Hash 会相同

参考:https://wiki.polkadot.network/docs/build-protocol-info#unique-identifiers-for-extrinsics

4.2. 手续费

Polkadot 的手续费计算方式如下所示:

fee = base_fee + length_of_transaction_in_bytes * length_fee + weight_fee
        ^                                             ^             ^
        |                                             |             |
    对每个 Tx                                    每个字节的费用    对 Pallet 中函数调用收的费用
    收的固定费用


final_fee = fee * fee_multiplier + tips
                         ^          ^
                         |          |
                   节点动态调节    用户设置的小费(可选的)
                   的一个乘数

参考:https://wiki.polkadot.network/docs/learn-transaction-fees

4.2.1. 估计手续费

通过 RPC payment_queryInfo 或者 payment_queryFeeDetails 可以估计交易的手续费。

5. 智能合约

Polkadot 作为中继链(Relay Chain),它本身不支持智能合约。 但,它的平行链(Parachains)支持智能合约。

平行链可以用下面两种方式支持开箱即用的智能合约:

  1. Frontier 提供的 EVM Pallet。
  2. 基于 Wasm 合约的 FRAME 库中的 Contracts Pallet。

参考:https://wiki.polkadot.network/docs/build-smart-contracts

6. Asset Hub(平行链)

The Polkadot Asset Hub is a parachain created for the common good, with the purpose of creating and sending fungible and non-fungible (NFTs) tokens throughout the entire Polkadot ecosystem.

比如,USDC 的 AssetId 是 1337

7. 附录

7.1. SS58 编码

下面 Python 代码实现了 SS58 编码,演示了从公钥推导出 polkadot 地址和通用 substrate 地址的过程:

# From: https://github.com/polkascan/py-scale-codec/blob/master/scalecodec/utils/ss58.py

""" SS58 is a simple address format designed for Substrate based chains.
    Encoding/decoding according to specification on
    https://docs.substrate.io/reference/address-formats/

"""
from typing import Optional, Union

import base58
from hashlib import blake2b


def ss58_decode(address: str, valid_address_type: Optional[int] = None) -> str:
    """
    Decodes given SS58 encoded address to an account ID
    Parameters
    ----------
    address: e.g. EaG2CRhJWPb7qmdcJvy3LiWdh26Jreu9Dx6R1rXxPmYXoDk
    valid_address_type

    Returns
    -------
    Decoded string AccountId
    """

    # Check if address is already decoded
    if address.startswith('0x'):
        return address

    if address == '':
        raise ValueError("Empty address provided")

    checksum_prefix = b'SS58PRE'

    address_decoded = base58.b58decode(address)

    if address_decoded[0] & 0b0100_0000:
        address_type_length = 2
        address_type = ((address_decoded[0] & 0b0011_1111) << 2) | (address_decoded[1] >> 6) | \
                       ((address_decoded[1] & 0b0011_1111) << 8)
    else:
        address_type_length = 1
        address_type = address_decoded[0]

    if address_type in [46, 47]:
        raise ValueError(f"{address_type} is a reserved SS58 format")

    if valid_address_type is not None and address_type != valid_address_type:
        raise ValueError("Invalid SS58 format")

    # Determine checksum length according to length of address string
    if len(address_decoded) in [3, 4, 6, 10]:
        checksum_length = 1
    elif len(address_decoded) in [5, 7, 11, 34 + address_type_length, 35 + address_type_length]:
        checksum_length = 2
    elif len(address_decoded) in [8, 12]:
        checksum_length = 3
    elif len(address_decoded) in [9, 13]:
        checksum_length = 4
    elif len(address_decoded) in [14]:
        checksum_length = 5
    elif len(address_decoded) in [15]:
        checksum_length = 6
    elif len(address_decoded) in [16]:
        checksum_length = 7
    elif len(address_decoded) in [17]:
        checksum_length = 8
    else:
        raise ValueError("Invalid address length")

    checksum = blake2b(checksum_prefix + address_decoded[0:-checksum_length]).digest()

    if checksum[0:checksum_length] != address_decoded[-checksum_length:]:
        raise ValueError("Invalid checksum")

    return address_decoded[address_type_length:len(address_decoded)-checksum_length].hex()


def ss58_encode(account_id: Union[str, bytes], address_type: int = 42) -> str:
    """
    Encodes an account ID to an Substrate address according to provided address_type

    Parameters
    ----------
    account_id: the public key, e.g. 0x192c3c7e5789b461fbf1c7f614ba5eed0b22efc507cda60a5e7fda8e046bcdce
    address_type: the address type, e.g. 0 for Polkadot, 2 for Kusama, 42 for generic Substrate. The prefix in https://github.com/paritytech/ss58-registry/blob/main/ss58-registry.json

    Returns
    -------
    str
    """
    checksum_prefix = b'SS58PRE'

    if address_type < 0 or address_type > 16383 or address_type in [46, 47]:
        raise ValueError("Invalid value for ss58_format")

    if type(account_id) is bytes or type(account_id) is bytearray:
        account_id_bytes = account_id
    else:
        account_id_bytes = bytes.fromhex(account_id.replace('0x', ''))

    if len(account_id_bytes) in [32, 33]:
        # Checksum size is 2 bytes for public key
        checksum_length = 2
    elif len(account_id_bytes) in [1, 2, 4, 8]:
        # Checksum size is 1 byte for account index
        checksum_length = 1
    else:
        raise ValueError("Invalid length for address")

    if address_type < 64:
        address_type_bytes = bytes([address_type])
    else:
        address_type_bytes = bytes([
            ((address_type & 0b0000_0000_1111_1100) >> 2) | 0b0100_0000,
            (address_type >> 8) | ((address_type & 0b0000_0000_0000_0011) << 6)
        ])

    input_bytes = address_type_bytes + account_id_bytes
    checksum = blake2b(checksum_prefix + input_bytes).digest()

    return base58.b58encode(input_bytes + checksum[:checksum_length]).decode()


if __name__ == "__main__":
    pub_key = "0x192c3c7e5789b461fbf1c7f614ba5eed0b22efc507cda60a5e7fda8e046bcdce"
    polkadot_address = ss58_encode(pub_key, 0)
    print("Polkadot address:", polkadot_address)  # 1a1LcBX6hGPKg5aQ6DXZpAHCCzWjckhea4sz3P1PvL3oc4F

    substrate_address = ss58_encode(pub_key, 42)
    print("Substrate address:", substrate_address)  # 5CdiCGvTEuzut954STAXRfL8Lazs3KCZa5LPpkPeqqJXdTHp

8. 参考

Author: cig01

Created: <2024-05-04 Sat>

Last updated: <2024-05-16 Thu>

Creator: Emacs 27.1 (Org mode 9.4)