XCMは、Polkadotエコシステム内で異なるコンセンサスシステム間が意思疎通するための「言語」であり、その重要性は言うまでもありません。『Gavin Wood:クロス・コンセンサス・メッセージフォーマット(XCM)の設計原理と動作メカニズムの詳細解説』では、Gavin Wood氏がXCMの設計思想と仕組みを詳細に解説しています。また、『Gavin Wood:XCMのバージョン管理と互換性に関する考察』では、バージョン管理と互換性について深く掘り下げています。
本稿では、Gavin Wood氏がXCMの基盤となる設計と実行モデルについて考察し、その根幹をなす仮想マシン(XCVM)の理解を深める手助けをします。

著者:Gavin Wood
出典:Polkadot
翻訳:Chen Yiwanfeng
XCMは、高度な仮想マシンであるXCVM上で動作する命令セットです。このアーキテクチャに慣れるため、まずXCVMの概要を簡単に説明します。
XCVMは、高度な非チューリング完全な仮想マシンです。スタックベースではなくレジスタベースで動作し、いくつかの専用レジスタを持ち、その多くは高度に構造化されたデータを保持します。汎用プロセッサとは異なり、XCVMのレジスタは任意の値に自由に設定できず、その変更方法は厳密に制御されています。ローカルチェーンの状態と相互作用する特定の方法(例えば既に紹介したWithdrawAssetやDepositAsset命令など)を除き、追加の「メモリ」は存在せず、ループや明示的な分岐命令もありません。
これまでの記事で、Holding Register(保持レジスタ)とOrigin Register(起源レジスタ)という2種類のレジスタを紹介しました。Holding Registerは1つまたは複数の資産を一時的に保持でき、ローカルチェーンから資産を引き出すか、信頼できる外部(例えば別のチェーン)から資産を受け取ることで内容を充填できます。Origin Registerは、XCM実行開始時にそのメッセージの発信元であるコンセンサスシステムの位置を保持し、内部の位置へ遷移するか完全にクリアするかのいずれかの操作のみが許可されます。
他のレジスタには、例外/エラー管理に関連するものが3つ、実行の重み(weight)を追跡するものが2つあります。本稿では、これらのレジスタを含む実行モデルに焦点を当てて解説します。
実行モデル
前述の通り、明示的な条件分岐命令や、同じ命令を繰り返し実行するためのループ構文は存在しません。これにより、プログラムの制御フローを事前に決定することが比較的容易になります。この特性は非常に有用です。なぜなら、XCMメッセージが実行される前に、その実行に必要な最大実行時間(Substrate/Polkadotでは「重み(weight)」と呼ばれます)を事前に把握したいからです。
XCMを実行するほとんどのコンセンサスプラットフォームでは、実行を開始する前に、最悪の場合の実行時間を確実に把握できる必要があります。ブロックチェーンは、単一ブロックの処理時間があらかじめ定められた上限を超えないことを保証しなければならず、そうでなければシステム全体が停止してしまうためです。さらに、システムが手数料を徴収する場合、その支払いは手数料計算対象のワークロードよりも前に発生しなければならず、かつこの支払いは最悪ケースの実行時間をカバーする必要があります。
Ethereumなどのチューリング完全な言語を採用するシステムでは、プログラムから直接最悪ケースの実行時間を計算することは事実上不可能です。そのため、ユーザーが事前にプログラムの実行リソースを指定し、実行時に消費量を計測し、支払った量を超えた時点で実行を中断するという手法を採用しています。ただし、トランザクションが実行される前に変更され、重み(weight)が不正確になる場合もあります。幸い、XCVMのような非チューリング完全な仮想マシンでは、このような実行時計測や重みの事前指定を回避することができます。
重み(Weight)
重みは、特定の操作を実行するために必要な標準的なハードウェアでの処理時間(ピコ秒単位)を表す整数値です。BuyExecution命令で見られるように、XCVMは命令を処理する際に、この「実行時間/重み」の概念を考慮に入れています。
重みを直接測定する仕組みはありませんが、XCVMプログラムが想定された最悪ケースの重みよりも少ない重みで完了できるように、「残余重みレジスタ」というレジスタが用意されています。多くの命令は使用する重みを正確に予測できるため、このレジスタを参照することはありません。しかし、最悪ケースの重み予測が実際よりも過大評価されてしまう場合があり、その過剰分は実行時になって初めて明らかになります。XCMメッセージの重みが過大評価されたブロックの実行時間を計算する際には、元の重みがどれだけ過大評価されていたかを記録し、その分をアカウントから差し引くことで、チェーンはブロック実行時間の割り当てを最適化できます。
このように、残余重みレジスタはブロック実行時間の管理に役立ちますが、支払い額が過大評価されないことを保証するという別の問題は、これだけでは解決できません。この問題を解決するには、BuyExecutionに関連する命令が必要で、余分に徴収された重みを返金する機能が求められます。幸い、そのような命令は既に存在しており、「残余返金(RefundSurplus)」と呼ばれています。この命令は「返金重みレジスタ」という第2のレジスタを使用し、同じ残余重みが複数回返金されないようにしています。
フロー制御と例外処理
これまで、XCVMの動作において暗黙的ですが非常に重要な役割を果たすレジスタが2つあります。1つは「プログラムレジスタ」で、現在実行中のXCVMプログラムを保持します。もう1つは「プログラムカウンタ」で、現在実行中の命令のインデックスを保持します。プログラムレジスタの内容が変更されると、プログラムカウンタはゼロにリセットされ、各命令が正常に実行されるたびに1ずつ増加していきます。
堅牢なコードを書く上で、リモートシステムで予期しない(あるいは予測不能な)事象が発生した場合に対処する能力、すなわち「例外」状況への対応は極めて重要です。たとえ単に元の状態に報告を送り返すだけだとしても、何らかの管理手段が必要になります。
XCVM命令セットには明示的な汎用分岐命令は含まれていませんが、その実行モデルには汎用的な例外処理フレームワークが組み込まれています。XCVMには、プログラムレジスタと同様にXCVMプログラムを格納する追加のコードレジスタが2つあり、それぞれ「付録レジスタ」と「エラーハンドラーレジスタ」と呼ばれます。一般的なプログラミング言語におけるtry/catch/finallyのような例外処理機構に慣れている方なら、以下の説明は理解しやすいでしょう。
前述の通り、XCVMプログラムは含まれる命令を1つずつ順番に実行します。すべての命令を実行し終えてプログラムの終端に達すると、次の2つのいずれかの結果になります:1) 正常にプログラム終端に到達する、2) エラーが発生する。最初の正常終了の場合、エラーレジスタはクリアされ、その重みが残余重みレジスタに加算されます。また、付録レジスタもクリアされ、その内容がプログラムレジスタに設定されます。プログラムレジスタが空であれば実行は停止し、空でなければプログラムカウンタがゼロにリセットされます。つまり、現在のプログラムとエラーハンドラー(存在すれば)を破棄し、付録プログラムの実行を開始するのです。
この機能だけではあまり有用ではありませんが、エラー発生時の挙動と組み合わせることで真価を発揮します。エラーが発生した場合、まだ実行されていない命令の重みがすべて残余重みレジスタに加算されます。エラーハンドラーレジスタはクリアされ、その内容がプログラムレジスタに設定され、プログラムカウンタはゼロにリセットされます。簡単に言えば、現在のプログラムを破棄し、エラーハンドラープログラムの実行を開始します。付録レジスタはクリアされないため、エラーハンドラーによってリセットされない限り、正常終了後に付録プログラムが実行されることになります。
この構造により、エラーハンドラーの任意の「ネスト」が可能になります。つまり、必要に応じてエラーハンドラー自体にも別のエラーハンドラーを設定でき、付録にも独自の付録を設定できるのです。
これらのレジスタを操作するための命令は2つあります:SetAppendixとSetErrorHandlerです。前者は付録レジスタを設定し、後者はエラーハンドラーレジスタを設定します。それぞれの予測重みは、パラメータの重みよりわずかに高く設定されています。ただし、実行時には、レジスタ内で置き換えられるXCMメッセージの重みが残余重みレジスタに加算されるため、未使用の付録やエラーハンドラーの重みを回収することが可能です。
エラーの発生(スロー)
場合によっては、意図的にエラーを発生させ、その内容をある程度カスタマイズできると便利です。これはテストコードの作成時によく使われますが、将来的には本番環境のチェーンでも活用される可能性があります。XCVMでは、Trap命令を使用することで常にエラーを発生させることができます。発生するエラーの種類も「Trap」という名前で、命令とエラーの両方に整数型のパラメータが含まれており、エラーを発生させる側と外部の観測者との間で何らかの情報をやり取りすることが可能です。
以下に簡単な例を示します:

Trapにより、本来のDepositAssetがスキップされ、代わりにエラー処理ルーチンのDepositAssetが実行されます。その結果、実行コストを差し引いた1 DOTがパラチェーン2000の管理下に置かれることになります。エラー処理ルーチンのコード冒頭では、常にRefundSurplusを使用することをお勧めします。なぜなら、このコードが実行されるということは、事前に予測されたウェイト(およびそれに基づいて購入されたウェイト)が過大評価されていた可能性が高いと判断できるからです。
エラー報告
エラー処理コードを導入することは非常に有用ですが、その際に求められる機能の一つが、XCMメッセージの結果を元の送信者に報告できることです。QueryResponse命令を使えば、あるコンセンサスシステムが別のシステムに対して情報を報告することが可能になります。課題は、XCMの結果を何らかの形でQueryResponseに組み込み、結果を知らせる必要のある相手に送信できるようにすることです。
実際、この目的を達成する命令はReportErrorただ一つです。ReportErrorは、これまで登場していない「エラーレジスタ」を利用して動作します。エラーレジスタはオプション型(設定またはクリア可能)で、設定されている場合、数値インデックスとXCMエラー型という2つの情報を含みます。
ReportErrorの動作メカニズムは極めてシンプルです。まず、命令がエラーを引き起こすたびに、エラーレジスタが必ず設定されます。この時、エラー型は発生したエラーの種類に、数値インデックスはプログラムカウンターレジスタの値にそれぞれ設定されます。第二に、エラーレジスタがクリアされるのは、ClearError命令が実行された時のみです。ClearError命令は、自身が決してエラーを引き起こさないという点で信頼性の高い命令です。つまり、エラー発生時に設定され、適切な命令によってクリアされる仕組みです。
これで、ReportError命令の動作原理が明確になったはずです。ReportErrorは、エラーレジスタの内容を使ってQueryResponse命令を構成し、それを指定された宛先に送信するだけです。ただし、ReportErrorの前にエラーが発生した場合、実行はまずエラー処理ルーチンレジスタのコードへジャンプし、その後付録(Appendix)レジスタのコードへジャンプするため、ReportError命令自体がスキップされてしまいます。この問題を解決する方法は簡単で、ReportErrorを付録(Appendix)内に配置すれば、メインコードでエラーが発生しても、必ず実行されることが保証されます。
簡単な例を見てみましょう。ここでは、資産(1 DOT)をリレーチェーンからStatemint(パラチェーン1000)へ送信し、そこで実行時間を購入した後、Statemintをリザーブとして利用して、資産をパラチェーン2000へ預け入れます。エラー報告機能がないオリジナルのメッセージは以下の通りです:

基本的なエラー報告機能を備えたバージョンでは、代わりに以下のようなメッセージを使用します:

ご覧の通り、唯一の変更点は、Statemintおよびパラチェーン2000におけるエラーや欠落をリレーチェーンへ報告するために、2つのSetAppendix命令を追加したことです。これは、リレーチェーンがすでに、Statemintおよびパラチェーン2000から送信されるQueryResponseメッセージ(クエリID 42、ウェイト制限1,000万)を認識・処理できるよう設定されていることを前提としています。幸い、これはSubstrateが優れたサポートを提供している機能ですが、本稿の範囲を超えるため、詳細は割愛します。
資産トラップ
資産を処理するプログラム内でエラーが発生した場合(多くの場合、BuyExecutionの実行料金を支払う必要があるため、これに該当します)、問題は深刻になります。BuyExecution命令自体がエラーを引き起こす可能性があり、これはウェイト制限が不適切であるか、支払いに使用する資産が不足していることが原因かもしれません。あるいは、資産が、それを適切に処理できないチェーンへ送信される可能性もあります。このような場合、XCVMの実行終了時に資産はHolding Registerに残ったままとなり、他のレジスタと同様に、これらの資産は一時的なものであり、そのまま忘れ去られることが期待されます。
チームやユーザーにとって朗報なのは、SubstrateのXCMがチェーンに対してこうした損失を完全に回避することを可能にしている点です。この仕組みは2段階で動作します。第一に、Holding Register内の資産がクリアされる際、それらは完全に忘れ去られるわけではありません。XCVMが停止した時点でHolding Registerが空でない場合、以下の3つの情報を含むイベントが発行されます:Holding Registerの値、Origin Registerの元の値、そしてこれら2つの情報のハッシュ値です。SubstrateのXCMシステムは、このハッシュ値をストレージに保存します。この仕組みの一部を「資産トラップ」と呼びます。
請求システム
このメカニズムの第二段階は、Holding Register に以前格納されていた資産の一部を「請求」できる点です。実際には、これを実現する特別な仕組みがあるわけではなく、汎用命令 ClaimAsset によって行われます。これは、Rust では以下のように宣言されます:

ClaimAsset という命令名は、WithdrawAsset や ReceiveTeleportedAsset といった他の「資金供給」関連の命令を連想させるかもしれません。同様に、この命令も引数 assets で指定された資産を Holding Register に格納しようと試みます。ただし、WithdrawAsset がアカウントのオンチェーン残高を直接減らすのに対し、ClaimAsset は Origin Register の値に関わらず、これらの資産に対する有効な「請求権」を探し出します。システムが請求権を見つけられるよう、ticket 引数を通じて追加情報を提供することも可能です。有効な請求権が見つかれば、それはチェーン上から削除され、対応する資産が Holding Register に追加されます。
では、「請求権」とは具体的に何を指すのでしょうか。これは完全に各チェーンに依存します。異なるチェーンは異なる種類の請求権をサポートでき、Substrate ではこれらを柔軟に組み合わせることができます。しかし、お察しの通り、あらかじめ用意されている特定の請求タイプがあります。それは、以前に破棄された Holding Register の内容です。
実際の動作を見てみましょう。例えば、ユーザーのパラチェーン2000がStatemintにメッセージを送信し、その主権アカウントから手数料として0.01 DOTを引き出し、さらに100単位のネイティブトークンがStatemintの主権アカウントに転送されたことを通知する場合を考えます。以下の図をご覧ください:

0.01 DOTの手数料が十分であり、Statemintがパラチェーン2000のローカル資産の預金(およびパラチェーン2000をリザーブチェーンとして利用すること)をサポートしていれば、この処理は成功します。しかし、Statemintがまだパラチェーン2000のネイティブ資産を認識する設定になっていない可能性もあります。その場合、DepositAsset は資産の処理方法が分からずエラーを発生させます。その後、平行チェーン2000にこの障害を通知する付録が実行され、結果として100単位のパラチェーン2000のローカル資産と、Holding Register に残る可能性のあるDOT(例えば手数料が0.005 DOTしか消費されなかった場合、残り0.005 DOT)が残ることになります。
その後、Statemint の XCM ダッシュボードは、これらの新たに請求可能となった資産に関するイベントを記録します。例えば以下のようになります:

そして、以下のようなメッセージがパラチェーン2000に送信されます:

パラチェーン2000は、後続の段階(例えば、Statemintが自チェーンのローカル資産の預金を受け入れ可能になった時点)で、比較的シンプルな方法でこの100単位を取り戻すことができます:

このケースでは、請求権を特定するための特別な情報として ticket 引数は提供されていません。これは通常、アセット・トラップ型の請求に適用されますが、他の種類の請求では必要になる場合もあります。
まとめ
今回の説明が、XCM の基盤となる仮想マシンの理解、および予期せぬ事態への対応や回復の仕組みについて、理解を深める一助となれば幸いです。本シリーズの次回では、XCM の将来の方向性とフォーマット改善の提案方法について触れ、さらに Substrate における XCM の Rust 実装の詳細と、あらゆるチェーンが容易に XCM を解釈できるようにする方法について掘り下げていきます。
