はじめに
PolkadotではNominated Proof of Stake(NPoS)というコンセンサスアルゴリズムを採用しておりますが、今回はこのNPoSをSubstrateで試してみます。
ただ、Substrateでブロックチェーンを構築するためのベースであるSubstrateテンプレートではデフォルトでProof of Authority(PoA)を採用しているため、NPoSは別で導入する必要があります。
今回の記事ではSubstrate node templateにNPoSを導入してみたのですが、結構手こずったのでどのように導入して環境構築していくか?を記事にしたいと思います。
導入手順
NPoSを導入するにあたり、Staking Palletを導入する必要がありますが、特に公式のドキュメントやチュートリアルなどはありません。ただ、導入された方もいてこちらの記事が参考になりますが、現状の最新のSubstrateテンプレートに対応していなかったため独自でStaking Palletを導入してみます。
今回の修正は上記記事のgithubソースを参考にSubstrateテンプレートとどのような違いがあるかを調査し、どのような修正をしたら良いか?を考えてNPoSを導入してみました。
最終的な修正後のソース
今回の最終的に修正したソースはこちらに上げておきました。
node部分のソース回収
Substrateテンプレートのnode箇所ではチェーン構成ファイル(chain_spec.rs)などのノードの基礎的なソースがありますが、これをNPoSに対応していく必要があります。
NPoSの導入がされているSubstrate本体のソースコードを元にします。以下コードに参考substrate側ソースをコピーします。
node/src/chain_spec.rs : 参考substrate側ソース
node/src/cli.rs : 参考substrate側ソース
node/src/command.rs : 参考substrate側ソース
node/src/rpc.rs : 参考substrate側ソース
node/src/service.rs : 参考substrate側ソース
node/Cargo.toml : 上記のソース修正に伴うpallet達の追加・更新など。substrateのtag=monthly-2022-02にする
以下ファイルは追加
node/res/flaming-fir.json : 参考substrate側ソース
Cargo.toml
palletのversionを揃えるのがややこしいです。
sp-finality-grandpa を grandpa-primitives という名前に統一します。
// sp-finality-grandpa = { version = "4.0.0-dev", git = "https://github.com/paritytech/substrate.git", tag ="monthly-2022-02" }
grandpa-primitives = { version = "4.0.0-dev", package = "sp-finality-grandpa", git = "https://github.com/paritytech/substrate.git", tag = "monthly-2022-02" }
同様に sc-finality-grandpa
// sc-finality-grandpa = { version = "0.10.0-dev", git = "https://github.com/paritytech/substrate.git", tag ="monthly-2022-02" }
grandpa = { version = "0.10.0-dev", package = "sc-finality-grandpa", git = "https://github.com/paritytech/substrate.git", tag = "monthly-2022-02" }
基本的には参考にしたSubstrate側ソースで全部上書きした後にCompileできるように整えていきました。
node_runtime -> node_template_runtime に修正
substrateをそのままコピーするとsubstrateソースのruntimeを参照するコードになっているので、これをSubstrateテンプレートのruntimeに合わせます。
[package]
name = "node-template-runtime"
以下が該当したので、これらのソースで node_runtime:: を node_template_runtime:: に置換します。
node/src/chain_spec.rs
node/src/command.rs
node/src/service.rs
command.rs
// use crate::{chain_spec, service, service::new_partial, Cli, Subcommand};
use crate::{chain_spec, service, service::new_partial, cli::Cli, cli::Subcommand};
rpc.rs
sc_finality_grandpa を grandpa に変更
// use sc_finality_grandpa::{
// FinalityProofProvider, GrandpaJustificationStream, SharedAuthoritySet, SharedVoterState,
// };
use grandpa::{
FinalityProofProvider, GrandpaJustificationStream, SharedAuthoritySet, SharedVoterState,
};
runtime側の修正
runtime/src/constants.rs : 参考substrate側ソース
runtime/src/impls.rs : 参考substrate側ソース
runtime/src/lib.rs : 参考substrate側ソース
runtime/src/voter_bags.rs : 参考substrate側ソース
runtime/Cargo.toml : 上記のソース修正に伴うpallet達の追加・更新など。substrateのtag=monthly-2022-02
Validator の Session Key を設定
Validator の Session Key を取得するためにscripts/prepare-test-net.shをダウンロードします。
prepare-test-net.shの修正
そのままだとSECRETがハードコーディングになってしまっているため以下のように修正します。
# Copy paste your mnemonic here.
# SECRET="panda dose welcome ostrich brief pull lawn table arrest worth ranch faculty"
if [ -z "$SECRET" ]; then
echo 'Please set the "SECRET" environment variable and try again.' >&2
exit 2
fi
また以下部分を(grandpa, babeなどの)種別を見やすくするために以下のように修正しておきます。
# printf "// $ADDRESS\nhex![\"${ACCOUNT#'0x'}\"].$INTO(),"
printf "// [$2] $ADDRESS\nhex![\"${ACCOUNT#'0x'}\"].$INTO(),"
その後、以下コマンドを打ちます。
その際にSECRET部分を任意のニーモニックに変更し、最後の数字部分を、作成するアカウント分の数に変更して(下記は3個のアドレスを作成)
% SECRET="panda dose welcome ostrich brief pull lawn table arrest worth ranch faculty" sh scripts/prepare-test-net.sh 3
(
// [stash] 5FNCTJVDxfFnmUYKHqbJHjUi7UFbZ6pzC39sL6E5RVpB4vc9
hex!["920c238572e2b31c2efd19dad1a5674c8188388d9a30d0d01847759a5dc64069"].into(),
// [controller] 5GgaLpTUcgbCTGnwVkCjSSzZ5jTaEPuxtWGRDhi8M1BP1hTs
hex!["cc4c78c7f22298f17e0e2dcefb7cff85b30e19dc1699cb9d1de00e5ea65a433d"].into(),
// [grandpa] 5Fm7Lc3XDxxbH4LBKxn1tf44P1R5M5cm2vmuLZbUnPFLfu5p
hex!["a3859016b0b17b7ed6a5b2efcb4ce0e2b6b56ec8594d416c0ea3685929f0a15c"].unchecked_into(),
// [babe] 5CyLUbfTe941tZDvQQ5AYPXZ6zzqwS987DTwFGnZ3yPFX5wB
hex!["2824087e4d670acc6f2ac4251736b7fb581b5bff414437b6abc88dc118ea8d5c"].unchecked_into(),
// [im_online] 5CahSqUXepwzCkbC7KNUSghUcuJxPDPKiQ4ow144Gb9qBPsX
hex!["16dffa9a82c7bb62f0f9929407223bf156458a4e7970ec4007ab2da7fb389f7d"].unchecked_into(),
// [authority_discovery] 5Eeard4qtNM8DBvqDEKn5GBAspbT7QEvhAjxSsYePB26XAiJ
hex!["724f3e6ec8a61ea3dc5b76c00a049f84fd7f212443b01241e0a2bb4ce503b345"].unchecked_into(),
),
:
:
上記で生成されたキーコードを node/src/chain_spec.rs の initial_authorities 部分に差し替えます。
ちなみに、こちらのキーは上から順に stash, controller, grandpa, babe, im_online, authority_discovery です。
Eraの時間変更方法
デフォルトの状態ですとValidatorの更新タイミングが1時間に1回です。この1時間の期間はEraと呼ばれています。
テストする際にこのEraが長いととてもテストしずらいため、Eraを以下のようにして短くします。
// pub const EPOCH_DURATION_IN_BLOCKS: BlockNumber = 10 * MINUTES;
pub const EPOCH_DURATION_IN_BLOCKS: BlockNumber = 1 * MINUTES;
Epochを10分に1回更新するのではなく、1分に1回更新する事になり、Eraの間隔が6分(Era = 6 * Epoch)になります。
こうしておくと早くValidatorリストの更新が確認しやすくなり、後での動作確認が素早くできるようになります。
Compile & チェーンファイルを作成
まず Compile して substrate の binary を作成します。
// compile
cargo build --release
Compileが成功したら、現状の設定でチェーンファイルを作成します。このファイルは後で使用します。
// Generate configuration file
./target/release/node-template build-spec --disable-default-bootnode --chain staging > customSpec.json
起動するノードの公開鍵を生成
subkeyとニーモニックを使ってノードの公開鍵を生成します。
% subkey inspect --scheme ed25519 "fire penalty pony chase gift loan grid mule tape wrestle stuff salute"
Secret phrase `fire penalty pony chase gift loan grid mule tape wrestle stuff salute` is account:
Secret seed: 0x09c047c99b49d03c96f5915497cc5e7ffb0fce31b732abf35f2d7d1bfd89de13
Public key (hex): 0x74a8cfbadb5d2b0178ec124791bfa8346ac3550a4f689923c806428090055277
Public key (SS58): 5EhfZPbz9JVAXWmHpKA1zc6jpBZXS3ExvmrvZrjZ6AMa2Vzq
Account ID: 0x74a8cfbadb5d2b0178ec124791bfa8346ac3550a4f689923c806428090055277
SS58 Address: 5EhfZPbz9JVAXWmHpKA1zc6jpBZXS3ExvmrvZrjZ6AMa2Vzq
こちらの公開鍵(Public key (hex))を元に以下のようにノードを起動します。
% ./target/release/node-template --tmp --dev --node-key 0x74a8cfbadb5d2b0178ec124791bfa8346ac3550a4f689923c806428090055277
2022-03-09 10:12:53 Substrate Node
2022-03-09 10:12:53 ✌️ version 4.0.0-dev-ea6a66e-x86_64-macos
2022-03-09 10:12:53 ❤️ by Substrate DevHub <https://github.com/substrate-developer-hub>, 2017-2022
2022-03-09 10:12:53 📋 Chain specification: Development
2022-03-09 10:12:53 🏷 Node name: extra-large-oven-1882
2022-03-09 10:12:53 👤 Role: AUTHORITY
2022-03-09 10:12:53 💾 Database: RocksDb at /var/folders/nh/fsz2n5z16753v95_x9ly7s_m0000gn/T/substrateP5eTIJ/chains/dev/db/full
2022-03-09 10:12:53 ⛓ Native runtime: node-268 (substrate-node-0.tx2.au10)
2022-03-09 10:12:53 [0] 💸 generated 1 npos voters, 1 from validators and 0 nominators
2022-03-09 10:12:53 🔨 Initializing Genesis block/state (state: 0x1053…8d4c, header-hash: 0xe291…b333)
2022-03-09 10:12:53 👴 Loading GRANDPA authority set from genesis on what appears to be first startup.
2022-03-09 10:12:53 👶 Creating empty BABE epoch changes on what appears to be first startup.
2022-03-09 10:12:53 Using default protocol ID "sup" because none is configured in the chain specs
2022-03-09 10:12:53 🏷 Local node identity is: 12D3KooWRm651Kd5GmsLTHJbgX5chQS5npx9ttLgo46UsegCMoNM
すると nodeのid(上記だと12D3KooWRm651Kd5GmsLTHJbgX5chQS5npx9ttLgo46UsegCMoNM)を表示するので、こちらを customSpec.json の BootNodes[] の部分に以下のように加えます。
{
:
"bootNodes": [
"/ip4/127.0.0.1/tcp/30333/p2p/12D3KooWRm651Kd5GmsLTHJbgX5chQS5npx9ttLgo46UsegCMoNM"
],
:
}
session keys のスクリプト追加
grandpaやbabeなどのkeyをチェーンに追加するためのスクリプトを追加します。
スクリプトはこちらのソースをダウンロードして scripts/session_keys に配置します。
audi, babe, gran, imolファイル作成&更新
また session_keys 配下のダウンロードした audi, babe, gran, imol のファイルを必要に応じて追加&修正します。
まず、作成するアカウント分だけ各種ファイルを用意し以下部分を修正します。
- 省略タイプ名 : audi, babe, gran, imol のいずれか
- ニーモニック : prepare-test-net.sh実行時に使用したSECRET
- 順番 : ファイル名の末尾数字
- タイプ名 : authority_discovery, babe, grandpa, im_online のいずれか
- HEX文字列 : prepare-test-net.sh実行時に出力した各タイプのHEX文字列(ただし0xを先頭につける
{
"jsonrpc":"2.0",
"id":1,
"method":"author_insertKey",
"params": [
"[省略タイプ名]",
"[ニーモニック]//[順番]//[タイプ名]",
"[HEX文字列]"
]
}
run.sh の修正
scripts/session_keys に run.sh が含まれています。
今までの手順通りであれば必要ありませんが、Validatorを4つにするなどする場合は、必要に応じて run.sh を修正します。
修正例としては、以下のような実装がされていますが port番号 や、-d で指定するデータファイル名の変更などです。
curl http://localhost:9935 -H "Content-Type:application/json;charset=utf-8" -d "@babe3"
Sudoアカウント設定
Sudoアカウント用にキーを設定します。
% subkey inspect --scheme sr25519 "royal novel glad piece waste napkin little pioneer decline fancy train sell"
Secret phrase `royal novel glad piece waste napkin little pioneer decline fancy train sell` is account:
Secret seed: 0x5b482eab1018eaee6293d3aaf15e4cc26fedd711b1ad4fe127adc11367ac3e9b
Public key (hex): 0xa2bf32e50edd79c181888da41c80c67c191e9e6b29d3f2efb102ca0e2b53c558
Public key (SS58): 5Fk6QsYKvDXxdXumGdHnNQ7V7FziREy6qn8WjDLEWF8WsbU3
Account ID: 0xa2bf32e50edd79c181888da41c80c67c191e9e6b29d3f2efb102ca0e2b53c558
SS58 Address: 5Fk6QsYKvDXxdXumGdHnNQ7V7FziREy6qn8WjDLEWF8WsbU3
customSpec.json の sudo箇所に Public key (SS58) を入れます。
"sudo": {
"key": "5Fk6QsYKvDXxdXumGdHnNQ7V7FziREy6qn8WjDLEWF8WsbU3"
},
ノード起動用のチェーンファイル作成
先ほどは--chainの指定をstagingしていたため、これを修正したcustomSpec.jsonをベースにチェーンファイルを再度作成します。
% ./target/release/node-template build-spec --disable-default-bootnode --chain customSpec.json > ./testnet.json
Validatorの数を調整
生成したtestnet.jsonのvalidatorCountを3、minimumValidatorCountはvalidatorCountと同じか低い値にしておきます(デフォルトで3になっていると思います)。
validatorCountは、実際に動くValidatorの数でこれ以上のValidator候補がいてもwaiting状態になってしまいます。
"staking": {
"validatorCount": 3,
"minimumValidatorCount": 3,
Validator起動
以下コマンドをターミナルを分けて実行します。
// First validator node
% ./target/release/node-template --chain testnet.json -d data/validator1 --name validator1 --in-peers 256 --validator --ws-external --rpc-external --rpc-cors all --rpc-methods=unsafe --node-key 0x74a8cfbadb5d2b0178ec124791bfa8346ac3550a4f689923c806428090055277
// Second validator node 2
% ./target/release/node-template --chain testnet.json -d data/validator2 --name validator2 --validator --port 30334 --ws-port 9946 --rpc-port 9934 --ws-external --rpc-external --rpc-cors all --rpc-methods=unsafe
// Third validator node
% ./target/release/node-template --chain testnet.json -d data/validator3 --name validator3 --validator --port 30335 --ws-port 9947 --rpc-port 9935 --ws-external --rpc-external --rpc-cors all --rpc-methods=unsafe
Session Key を挿入
Validatorを起動したあとに、以下コマンドを実行して事前に準備しておいたSession Key情報をチェーンに挿入します。
% cd scripts/session_keys
% sh run.sh
Session Keyの挿入が成功すると以下のようにブロックが進んでいきます。
finalizedが進まないことがあるので、その場合はValidatorノードを再起動してください。
2021-06-11 09:40:45 ✨ Imported #3 (0xf18c…97f0)
2021-06-11 09:40:48 ✨ Imported #4 (0x4bdc…aa87)
2021-06-11 09:40:48 💤 Idle (2 peers), best: #4 (0x4bdc…aa87), finalized #1 (0xad8e…f0aa), ⬇ 5.0kiB/s ⬆ 4.8kiB/s
2021-06-11 09:40:51 ✨ Imported #5 (0x4064…6d67)
2021-06-11 09:40:53 💤 Idle (2 peers), best: #5 (0x4064…6d67), finalized #3 (0xf18c…97f0), ⬇ 2.5kiB/s ⬆ 2.2kiB/s
画面確認
ブラウザで
https://polkadot.js.org/apps/?rpc=ws%3A%2F%2F127.0.0.1%3A9944#/staking
にアクセスするとStaking画面が表示されます。
これでNPoS環境が整いました。
確認手順
Validatorの追加や削除などをして動作を確認していきます。
Validator追加
まず新しいアカウント用の12個の文字列のニーモニックを用意しておきます。ニーモニックはこちらのサイトで生成できます。
ここでは
pulp nose again buddy edge foam aim atom hello follow gauge maze
とします。
Polkadot{.js} extensionをブラウザ(ここではchromeを想定)にインストールします。
インストール後に、Extensionを起動して[Import account from pre-existing seed]を選らびます。
そして、先ほど取得したニーモニックを入れ画面の指示に従いアカウントを作成します。
指定したニーモニックでアカウントが作成できました。
追加Validatorノード起動
ファイル作成
新たに追加する session keys の audi, babe, gran, imolファイル 作成します。
scripts/session_keys/audi4
scripts/session_keys/babe4
scripts/session_keys/gran4
scripts/session_keys/imol4
作成方法は上記で紹介した方法を見て下さい
また[HEX文字列]を取得するために先ほどのニーモニックを使ってアカウントキー情報を出してファイルに入れ込みます。
[ニーモニック][順番]も変更する必要があるので注意してください。
% SECRET="pulp nose again buddy edge foam aim atom hello follow gauge maze" sh scripts/prepare-test-net.sh 1
Validator起動
新たに追加するValidatorを起動します。
ブロック同期が一旦はじまりますが、すぐに停止します。
% ./target/release/node-template --chain testnet.json -d data/validator4 --name validator4 --validator --port 30336 --ws-port 9948 --rpc-port 9936 --ws-external --rpc-external --rpc-cors all --rpc-methods=unsafe
:
:
2022-03-11 11:47:54 💤 Idle (1 peers), best: #11 (0x1989…3405), finalized #9 (0x069d…b64b), ⬇ 0.9kiB/s ⬆ 0.9kiB/s
2022-03-11 11:47:59 💤 Idle (1 peers), best: #11 (0x1989…3405), finalized #9 (0x069d…b64b), ⬇ 0.8kiB/s ⬆ 0.8kiB/s
キー挿入
上記ファイルを追加したあとに以下コマンドで key を insert します。
% cd scripts/session_keys
% curl http://localhost:9936 -H "Content-Type:application/json;charset=utf-8" -d "@babe4"
% curl http://localhost:9936 -H "Content-Type:application/json;charset=utf-8" -d "@gran4"
% curl http://localhost:9936 -H "Content-Type:application/json;charset=utf-8" -d "@imol4"
% curl http://localhost:9936 -H "Content-Type:application/json;charset=utf-8" -d "@audi4"
すると、ブロック同期が再開されます。
注意
GRANDPA voter error: Signing("Failed to sign GRANDPA vote for round 2 targetting 0xa2fedd20813e1bdcf0bd99f64ad907132ff5d6fd3cb00e2ed0ad1261664c01a1")
のようなエラーが追加ノードで出力された場合は、session keyの挿入がうまくいっていません。
ファイルの中身、もしくはprepare-test-net.shの修正が正しく行われているか確認してください。
Staking
初期残高の準備をする
Validatorノードが起動していたら、全て一度停止し、データを一度全部削除してしまいます。
% rm -rf data/validator*/**
testnet.json の 以下配列に先ほど作成したアカウントのアドレスと残高を追加します。
"balances": {
"balances": [
:
[
"5FjB53jPiNqSTSGa6RK3fAKaBmMxCaJrrxaKbm6jJpzWbqq1",
5000000000000000000000
]
その後、全validator起動して、session keyを挿入し、 全validatorを再起動します。
こちらに再起動ソースを用意しておきました。適宜修正して使ってください。
% ./scripts/restart-env.sh
Stakingをする
追加ノードをValidatorノードとして認識させるためにStakingしていきます。
[Account actions]タブを押した後に[+ Validator]ボタンを押します。
Stakingは初期ValidatorのStake量よりも多くしておきます。
Stakingを行うために rotateKey が必要になるため以下のように新たに追加するノードに curl でアクセスして取得しておきます。
$ curl -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method": "author_rotateKeys", "params":[]}' http://localhost:9936 | jq .
{
"jsonrpc": "2.0",
"result": "0x229f73aae462af30df2fc2f4bdfb57360af224787b65ce656f44a3b995be706e34d48bdf307c33da5b8a381871bd9a816d3f0ab56ebb6b954bf67b34a8d2314084814b71b3b734bc4610970fc3de342cbcbf655337c8eadbdc9f116d07788f1b18852ee79d75d0c06fa4e9a8e529123d9f72d59e70b21b7c4aad2fa0393d4663",
"id": 1
}
画面の指示に従って tx を発行します。
無事Stakingが行われると以下画面が表示されます。
またStakingを行うとWaitingにノードが表示されます。
この時点ではまだValidatorとして認識されていません。
しばらくするとEraが更新されます。その際にValidatorセットの見直しが行われて追加ValidatorノードがValidatorとして認識されます。
これでValidatorリストへValidatorの追加が行えました。
Validatorを削除する
ValidatorリストからValidatorを削除してみます。
追加したValidatorに対してChillを行います。
下記画面のように[Account Actions]タブを開いて、Stopを押します。
すると下記画面のように Sending transaction staking.chill() するか聞かれるのでtx発行します。
すると、stopボタンがなくなり、Validate もしくは Nominateボタンが表示されるようになります。
これでChillした事になります。
このChillをした後に、Eraが更新されると先ほどリストから外されWaitingになっていたValidatorが戻り、新しく追加ValidatorはWaitingからも外れます(ChillすることによってStakeが外されたため)
終わりに
簡単にですがNPoS環境でStakingやChillによりValidatorリストへのValidator追加、削除を試してみました。
まだまだ、Nominateなど試していない動作はありますが、今回の手順を行えばNPoS環境はとりあえず整うので、自分で色々と動作を試しても良いかもしれません。
今回のソースはこちらに上げておきました。
余談
customSpec.jsonのminimumValidatorCountはこの数よりValidator稼働していないとチェーンが停止するらしいのですが、validatorCountより動いているvalidatorの数が少ないと Era が切り替わらないので、この値の詳細がよくわからずです。
また、NPoSを使う際に予め稼働するValidator数を設定する必要がありそうで、チェーン稼働中にValidatorセットの最大数を増やせるかはまだ良く分かってません。。。