👮‍♀️

PolkadotのRuntime Upgradeをモニタリングする

2023/03/16に公開

この記事は株式会社 Ginco のテックブログとして書いています。

この記事では、Polkadot のノード運営やウォレットアプリケーションを運用するにあたって、非常に重要なモニタリング項目の一つである「Runtime Upgrade のモニタリング」をどのように行う方法があるかについて、検証・紹介していこうと思います。

Runtime とは

Runtime とは、Polkadot と Substrate におけるブロックチェーンのロジックや機能を定義するプログラムのことで、Runtime は WebAssembly (WASM)というバイナリ形式でコンパイルされ、ブロックチェーンノード上の仮想マシンで実行されます。


引用: https://docs.substrate.io/fundamentals/architecture/#high-level-overview

Runtime には、トランザクションの検証方法、アカウントの管理方法、コンセンサスの仕組みなどが含まれており、各機能は Pallet という単位でモジュール化されており、必要な機能を選択して組み合わせることができます。

Runtime Upgrade とは

Polkadot(Substrate)以外のブロックチェーンの多くは、コアロジックの修正の際にフォーク(ハードフォークやソフトフォーク)を必要とし、新しい変更に対応したノードソフトウェアがフォークのたびにリリースされます。

それにより、後方互換性の無い変更が加えられた場合に、その変更を含まないノードは、他のノードとの合意を維持できなくなるために、ハードフォークが発生します。ハードフォークはそのアップグレードの性質により、ガバナンス的にもロジック的にも厄介な問題が発生する可能性があります。

一方、Polkadot(Substrate)に導入されている Forkless Upgrade はオンチェーン上にコアロジックや機能を定義した WASM コードを配信することにより、数千のノードオペレーターにフォークのたびにアップグレードを要求する調整の問題を解決しています。

また、Runtime Upgrade は Polkadot のステークホルダーが、オンチェーンのガバナンスシステムを介してアップグレードを提案し承認されると、自律的に実行されます。

Runtime Upgrade のモニタリング

Runtime Upgrade は、ブロックチェーンの機能性やセキュリティに直接関係する更新であり、それらのアップグレードが正常に機能することを確認する上で、日常的にモニタリングを行うことが非常に重要となってきます。

まず、Polkadot や Substrate のエコシステムは非常に活発であり、新しいプロトコルや機能が継続的に導入されています。最新のアップグレードに関する情報を把握することで、新しい機能やプロトコルについての理解を深め、それらを活用することができたり、新しい機能によってアプリケーションの改修が必要となる場合に、早めのアクションを取ることができるようになります。

さらに、アップグレードには、既存のノードに対して互換性がない場合があるため(直近でいうと WeightV2 の導入で一部 API に後方互換性が無くなっていた)、アップグレードに関する情報を把握し、必要な手順を踏んでアップグレードを実行することで、ノードの更新や運用を円滑に行うことができます。

以上のような理由から、Polkadot や Substrate の Runtime Upgrade をモニタリングすることは、非常に重要であるといえます。

では次項から、具体的な Runtime Version(Spec Version)の取得方法および Runtime Upgrade のモニタリング方法を紹介していきます。
なお、サンプルコードは javascript を使用します。

Runtime Upgrade の発生タイミングをモニタリングする3つの方法

Runtime は特定のバージョンを持ち、それを RuntimeVersion(specVersion) と呼びます。

RuntimeVersion が変更されると、以前のバージョンとは異なるルールがチェーン上にて適用されます。これは、以前のバージョンで生成されたブロックが新しいバージョンで無効になる可能性があることを意味します。
したがって、RuntimeVersion が変わるということはつまり、フォークが発生しうるタイミングとなるので、ノード運用者やアプリケーション運用者は RuntimeVersion(specVersion) を定期的にモニタリングすることでサービスの正常性を確認する必要があります。

RuntimeVersion(specVersion) のモニタリング方法には以下の3つのアプローチがあります。

  1. RPC「state.getRuntimeVersion」で specVersion を取得する
  2. Storage の「system.lastRuntimeUpgrade」から specVersion を取得する
  3. websocket を用いて「chain_subscribeRuntimeVersion」を subscribe する

方法 1: RPC「state.getRuntimeVersion」で specVersion を取得する

まず 1 つ目は RPC メソッドの state.getRuntimeVersion を使用して Spec Version(Runtime Version)を取得する方法です。

  • curl を使用する場合

    curl
    curl --request POST \
    	--url https://rpc.polkadot.io \
    	--header 'Content-Type: application/json' \
    	--data '{
    		"jsonrpc": "2.0",
    		"method": "state_getRuntimeVersion",
    		"params": [],
    		"id": 1
    	}'
    
    ##### Return #####
    {
    	"jsonrpc": "2.0",
    	"result": {
    		"specName": "polkadot",
    		"implName": "parity-polkadot",
    		"authoringVersion": 0,
    		"specVersion": 9370,
    		"implVersion": 0,
    		"apis": [...],
    		"transactionVersion": 20,
    		"stateVersion": 0
    	},
    	"id": 1
    }
    ##################
    
  • polkadot-js(javascript)を使用する場合

    javascript
    // Import the API
    const { ApiPromise, WsProvider } = require("@polkadot/api");
    
    async function main() {
    // Initialise the provider to connect to the local node
    const provider = new WsProvider("wss://rpc.polkadot.io");
    // Create our API with a default connection to the local node
    const api = await ApiPromise.create({ provider });
    
    const runtimeVersion = await api.rpc.state.getRuntimeVersion();
    console.log(JSON.stringify(runtimeVersion));
    }
    
    main()
    .catch(console.error)
    .finally(() => process.exit());
    
    // Return: {"specName":"polkadot","implName":"parity-polkadot","authoringVersion":0,"specVersion":9370,"implVersion":0,"apis":[...],"transactionVersion":20}
    

方法 2: Storage の「system.lastRuntimeUpgrade」から specVersion を取得する

2 つ目は RPC メソッドの state.getRuntimeVersion を使用して Storage から Spec Version(Runtime Version)を取得する方法です。

  • polkadot-js(javascript)を使用する場合

    // Import the API
    const { ApiPromise, WsProvider } = require("@polkadot/api");
    
    async function main() {
      // Initialise the provider to connect to the local node
      const provider = new WsProvider("wss://rpc.polkadot.io");
      // Create our API with a default connection to the local node
      const api = await ApiPromise.create({ provider });
    
      const lastRuntimeUpgrade = await api.query.system.lastRuntimeUpgrade();
      console.log(lastRuntimeUpgrade);
    }
    
    main()
      .catch(console.error)
      .finally(() => process.exit());
    
    // Return: {"specVersion":9370,"specName":"polkadot"}
    

方法 3: websocket を用いて「chain_subscribeRuntimeVersion」を subscribe する

3 つ目は Websocket のエンドポイントに対して、RPC メソッドの chain_subscribeRuntimeVersion を使用して Spec Version(Runtime Version)をサブスクライブする方法です。
こちらの方法では、Runtime Upgrade が実行されるごとに、state_runtimeVersion のレスポンスが流れてきます。

  • wscat を使用する場合
wscat --connect wss://rpc.polkadot.io
Connected (press CTRL+C to quit)
> {"jsonrpc": "2.0","id": "1","method": "chain_subscribeRuntimeVersion","params": []}
< {"jsonrpc":"2.0","result":"S09ZNcTQdAQk1FQC","id":"1"}
< {
    "jsonrpc": "2.0",
    "method": "state_runtimeVersion",
    "params": {
        "subscription": "S09ZNcTQdAQk1FQC",
        "result": {
            "specName": "polkadot",
            "implName": "parity-polkadot",
            "authoringVersion": 0,
            "specVersion": 9370,
            "implVersion": 0,
            "apis": [...],
            "transactionVersion": 20,
            "stateVersion": 0
        }
    }
}

事前に Runtime Upgrade のタイミングを知る

前項では、RuntimeVersion を定期的にモニタリングすることによって、Runtime Upgrade がいつ発生したかをモニタリングする方法を紹介しました。
しかし、アップグレードされる Runtime の内容によっては、事前にアップグレード内容を把握し、アプリケーションの修正やノードのアップデートを行う必要があるかもしれません。
前項の方法では事前にいつ Runtime が Upgrade されるかは把握できないので、今回はブロックチェーン上の投票(democracy)情報を取得することにより、いつ Runtime Upgrade が実施されるかを知る方法を紹介します。

ブロック内の democracy イベントを取得し、Referendum(投票)の ID を取得

今回は例として、ReferendumIndex: 107 の投票が始まったブロックの情報を取得します。
該当ブロックのリンクはこちら: https://polkadot.subscan.io/block/14442718

// Import the API
const { ApiPromise, WsProvider } = require("@polkadot/api");

async function main() {
  // Initialise the provider to connect to the local node
  const provider = new WsProvider("wss://rpc.polkadot.io");
  // Create our API with a default connection to the local node
  const api = await ApiPromise.create({ provider });

  const blockHash = await api.rpc.chain.getBlockHash(14442718);
  const apiAt = await api.at(blockHash);
  const events = await apiAt.query.system.events();

  events.forEach(({ event }) => {
    // ここで、Block内のEventからdemocracy.Startedのイベントのみをクエリしています
    if (api.events.democracy.Started.is(event)) {
      const [refIndex, threshold] = event.data;
      console.log(`A referendum has begun. [${refIndex}, ${threshold}]`);
    }
  });
}

main()
  .catch(console.error)
  .finally(() => process.exit());

// Return: A referendum has begun. [107, SimpleMajority]

これで、14442718ブロックにおいて、ReferendumIndex 107 の投票が始まったことが確認できます。
このように、ブロックごとに Event を取得して、democracy のイベントを監視する必要があります。

投票イベントの詳細を確認

Referendum(投票)が開始されるのは、何も Runtime Upgrade の時だけではありません。
よって、この Referendum で実行されようとしているのは何なのかを取得する必要があります。

// Import the API
const { ApiPromise, WsProvider } = require("@polkadot/api");

async function main() {
  // Initialise the provider to connect to the local node
  const provider = new WsProvider("wss://rpc.polkadot.io");
  // Create our API with a default connection to the local node
  const api = await ApiPromise.create({ provider });

  // 投票状況はブロックが生成されるごとに進行していく
  const blockHash = await api.rpc.chain.getBlockHash(14442718);
  const apiAt = await api.at(blockHash);
  const referendum = await apiAt.query.democracy.referendumInfoOf(107);

  console.log(JSON.stringify(referendum));
}

main()
  .catch(console.error)
  .finally(() => process.exit());
取得した投票情報
  • 取得した投票情報
    • 投票開始(ブロック高: 14442718)
      {
        "ongoing": {
          "end": 14543518,
          "proposal": {
            "lookup": {
              "hash": "0x56ee5c8542848bfa5c8f5be09ebf74857984031841b50679f2d5009ecfb41f6e",
              "len": 1328944
            }
          },
          "threshold": "SimpleMajority",
          "delay": 400,
          "tally": { "ayes": 0, "nays": 0, "turnout": 0 }
        }
      }
      
    • 投票締め切り直前(ブロック高: 14543517)
      {
        "ongoing": {
          "end": 14543518,
          "proposal": {
            "lookup": {
              "hash": "0x56ee5c8542848bfa5c8f5be09ebf74857984031841b50679f2d5009ecfb41f6e",
              "len": 1328944
            }
          },
          "threshold": "SimpleMajority",
          "delay": 400,
          "tally": {
            "ayes": "0x00000000000000000016b45bc7ceb96d",
            "nays": 6463558200000,
            "turnout": "0x0000000000000000001bf6ba3dede5b7"
          }
        }
      }
      
    • 投票締め切り(ブロック高: 14543518)
      { "finished": { "approved": true, "end": 14543518 } }
      

ReferendumInfo で取得できる情報には、以下のような情報が含まれています。

  • end
    • 投票終了のブロック高
  • proposal
    • 投票内容
    • 投票可決時に実行される Extrinsic が PreImage という形で Storage に格納される(PreImage については次のセクションで詳解します)
  • threshold
  • delay
    • 投票終了から投票内容が有効化されるまでの執行猶予ブロック数
    • 今回の場合だと、投票が可決された場合に proposal がend + delay (14543518 + 400) = 14543918に有効化される

Preimage の内容を取得

前述したように、Referendum には、投票可決時に実行される内容が Preimage(Extrinsic)として含まれています。
その Preimage の内容によって Runtime が Upgrade されたりなどのチェーンへの変更が適用されます。

// Import the API
const { ApiPromise, WsProvider } = require("@polkadot/api");

async function main() {
  // Initialise the provider to connect to the local node
  const provider = new WsProvider("wss://rpc.polkadot.io");
  // Create our API with a default connection to the local node
  const api = await ApiPromise.create({ provider });

  // referendum:107のPreImage[0x56ee5c8542848bfa5c8f5be09ebf74857984031841b50679f2d5009ecfb41f6e]が登録されたのは
  // ブロック高: 14487460 だったため、apiAtで14487460時点の情報を取得するように設定
  // https://polkadot.subscan.io/extrinsic/14487460-2
  const blockHash = await api.rpc.chain.getBlockHash(14487460);
  const apiAt = await api.at(blockHash);
  const referendum = await apiAt.query.democracy.referendumInfoOf(107);
  const readableReferendum = referendum.toJSON();

  // Preimgae(Extrinsic)の内容を取得
  const preimage = await apiAt.query.preimage.preimageFor([
    readableReferendum.ongoing.proposal.lookup.hash,
    readableReferendum.ongoing.proposal.lookup.len,
  ]);
  const preimageHex = preimage.toString();

  // PreimageはそのままだとHexのデータなので、Decode
  const extrinsicCall = api.createType("Call", preimageHex).toJSON();
  // 可読性向上のため、CallIndexに対応したMethodとSectionをMetadataから取得しています
  const parsedCall = findCallMethodAndSection(apiAt, extrinsicCall);

  console.log(JSON.stringify(parsedCall));
}

function findCallMethodAndSection(api, call) {
  for (key in call) {
    if (!call.hasOwnProperty(key)) {
      continue;
    }
    // CallIndexに対応した、CallデータをMeatadataから取得
    if (key == "callIndex") {
      const { method, section } = api.registry.findMetaCall(
        Uint8Array.from(Buffer.from(call[key].replace(/^0x/, ""), "hex"))
      );
      call["method"] = method;
      call["section"] = section;
    }
    if (typeof call[key] == "object") {
      findCallMethodAndSection(api, call[key]);
    }
  }
  return call;
}

main()
  .catch(console.error)
  .finally(() => process.exit());

このスクリプトを実行した結果が以下になります。

{
  "callIndex": "0x1a05",
  "args": {
    "call": {
      "callIndex": "0x0002",
      "args": { "code": "..." }, // WASMのバイトコードのため、省略。
      "method": "setCode",
      "section": "system"
    },
    "weight": { "refTime": 1000000000000, "proofSize": 3145728 }
  },
  "method": "withWeight",
  "section": "utility"
}

以上のスクリプト結果より、今回の Referendum が可決されると、utility.withWeight が実行されることになっていることが確認できます。
またutility.withWeightargsに渡された Call を実行する関数なので、system.setCode、つまり Runtime Upragde を行う Referendum だと確認することができました。

今回取得した Referendum:107 の情報まとめ

  • 投票はブロック高:14442718 から始まり、ブロック高:14543518 で締め切られる
  • 投票が可決された場合は、投票終了ブロック高から 400 ブロック経過(Delay)したタイミングで Preimage が実行
  • Preimage の内容はutility.withWeightで Wrap されたsystem.setCodeが実行されるというもの

まとめ

今回は、Polkadot(Substrate)での Runtime Upgrade をモニタリングする方法について、紹介しました。

今記事で紹介した、Runtime Upgrade のモニタリングでの基本的な考え方は「RuntimeVersion 情報の定期取得」×「投票情報の定期取得」と整理することができます。

Polkadot(Substrate)以外のブロックチェーンの多くは、コアロジックの修正の際にフォーク(ハードフォークやソフトフォーク)を必要とし、ノードソフトウェアのリリース時にフォークタイミングがハードコードされることが多いので、フォークのタイミングを把握することが容易ですが、Polkadot(Substrate)のようなフォークレスアップグレードではブロックチェーン上の投票次第でコアロジックが修正されるので、ブロックチェーン上のアクティビティを注意深く監視しておく必要があります。

Polkadot(Substrate)を用いたアプリケーションを運用している方やノードを運用している方にとって、当記事で紹介した Runtime Upgrade のモニタリング方法が少しでも参考になれば幸いです。

感想やコードや内容についてのご指摘やアドバイスなどは Twitter(@nandehu0323)にいただけると非常にありがたいです!

株式会社 Ginco ではブロックチェーンを学びたい方、ウォレットについて詳しくなりたい方を募集していますので下記リンクから是非ご応募ください。

https://herp.careers/v1/ginco

参考文献

https://docs.substrate.io/fundamentals/architecture/

https://wiki.polkadot.network/docs/build-protocol-info#runtime-upgrades

https://wiki.polkadot.network/docs/learn-runtime-upgrades#client-releases

https://wiki.polkadot.network/docs/learn-governance

https://polkadot.js.org/docs/

Discussion