・東京で開催されたXymposium 2023での講演録

・Symbol(とNEM)の動機のいくつかについて

・Catapultクライアントのアーキテクチャの概要

をdeepl翻訳で日本語変換したのを下記にまとめます。


 ↓掲載元



 コンニチハ ご挨拶とご挨拶 ありがとうございます!

 本日は、初めてのXYMPOSIUMにご参加いただきありがとうございます。 皆さんが楽しい時間を過ごし、多くのことを学んでくださることを願っています。 NEMの開発が本格的に始まってから遅かれ早かれ10年近く経ちます。本日直接ご一緒できなかったのは残念ですが、私は今年の3月に来日し、美しい日本の多くを体験することができました。


 この講演では、Catapultクライアントのアーキテクチャの概要と、あまり知られていない機能を紹介します。 その前に、シンボルの動機について簡単にお話ししたいと思います。


 シンボルを理解するためには、NEMが始まった2014年初頭までタイムマシンで戻ってみる必要があります。 当時はビットコインのハイプサイクルの末期で、アルトコインの爆発的な増加を伴っていました。 ビットコインはウォール街のおもちゃになるずっと前で、まだほとんどが非中央集権的で、本来の原則にかなり近い形で調整されていました。 その時代にはNXTというコインがあり、巧妙なProof of Stakeアルゴリズムを持っていましたが、非常に有害なコミュニティと非常に集中的な流通をしていました。 NXTコミュニティのメンバーの一人が、NEMを構築するために開発者を募集しました。 私たちはビットコインとNXTの良い面を改善し、前向きなコミュニティを作りたかったのです。 当時はプルーフ・オブ・ステーク(Proof of Stake)の黎明期で、何が可能なのかを確かめたかったのです。 主に、使いやすくて楽しいものを作りたかったのです。


 NEMは2015年3月にローンチされ、特に日本では驚くほどの成功を収めました。 それでも、NEMはエンタープライズグレードのものというよりは、私たちのサイドプロジェクトとして常に作られていました。 NEMクライアントとNEMプロトコルの一部は急ぎ足で作られました。


 NEMクライアントはすべてJavaで書かれました。 当時でさえ、Javaがベストな選択ではないことはわかっていましたが、初期に貢献することに興味を示してくれた一握りの開発者に配慮してJavaを選びました。 結局のところ彼らは去り、私たちはガベージ・コレクタとの戦いから抜け出せなくなりました。ガベージ・コレクタは、警告なしにノードを数秒間フリーズさせる可能性がありました。 言語の冗長さと "安全性 "もまた、私たちの活動をを鈍らせました。


 並列化には限界がありました。 並列化されたのは1つのブロックチェーン内の署名の検証だけでした。

 トランザクションのシリアライゼーションは適切にチューニングされておらず、固定サイズと可変サイズのバイナリデータを区別していませんでした。 その結果、固定サイズのデータにはしばしば冗長なサイズ情報が付加され、ペイロードサイズが無意味に増大しました。 同様に、トップレベルトランザクションと埋め込みトランザクションの区別がなかったため、すべてのマルチシグトランザクションは埋め込みトランザクションヘッダからの冗長な情報を含んでいました。


 データストレージには多くの不満が残りました。 トランザクションとブロックはH2データベースに格納されていましたが、計算された状態は格納されていませんでした。 その結果、ノードが再起動するたびに状態をその場で再計算する必要がありました。 これが現在NEMノードの起動に数時間から数日かかる理由です。 さらに、ステートハッシュがなかったため、ステートの証明は不可能でした。

 ブロック内にはMerkleトランザクションハッシュすらなかったため、ブロックヘッダだけを素早く同期し、後でトランザクションデータをダラダラと引き出すことは不可能でした。


 2016年、ある日本企業が私たちのスポンサーになることに同意し、NEMの構築から学んだことを活かして、セキュリティ、パフォーマンス、モジュール性を一から考慮して構築されたエンタープライズグレードのブロックチェーンを開発することになりました。 これがSymbolになったのです。 使いやすさと楽しさは相変わらず私たちの北極星でしたが、私たちは非常に(おそらく過度に)拡張性の高いクライアントも求めていました。 そして、それがCatapultになったのです!

 CatapultはSymbolのリファレンスクライアントで、C++で書かれています。 これはC++ 17で構築されており、ラムダやあらゆるものを備えたC++をモダンな言語にしています!

 不必要な肥大化を避けるため、バイナリ・データ構造のレイアウトの最適化にかなりの時間を費やしました。 加えて、リトルエンディアン・マシン上で構造体をシリアライズしないようにしたかったのです。 言い換えれば、生データを正しい型のポインタにキャストするだけで、シリアライズのオーバーヘッドなしにデータに即座にアクセスできるようにしたかったのです。 そのためには、バイナリ定義が完璧なバイト忠実度を持つ必要がありました。

 残念ながら、protobufのような "万能 "シリアライゼーション・ライブラリは、これをサポートできないことがわかりました。 ほとんどのライブラリは、未加工のバッファへのアクセスを返さず、また、余計なセンチネルバイトやサイズバイトを挿入してしまうものもありました。

 この目標を達成するため、構造を使いやすくするため、私たちはcatbufferというカスタムドメイン固有言語(DSL)を作りました。 そのスキーマファイルでは、各ペイロードのバイナリレイアウトを完全なバイト忠実度で指定します。 公式のJavaScriptとPython SDKには、catbufferスキーマファイルを入力し、JavaScriptまたはPythonでそれぞれ対応する型を出力するジェネレータが含まれています。 このようにして、一旦catbufferスキーマで新しいトランザクションを定義すると、公式のJavaScriptとPython SDKは、それぞれのジェネレータを実行した後、直ちにそれをサポートすることができます。 最終的には、DSLを拡張してNEMトランザクションとその奇妙な癖もすべてサポートするようにしました。 これにより、NEMとSymbolの両方をサポートするデュアルSDKを簡単にサポートできるようになりました。


 Catapultでは、ノードは互いにTCP上でSSLコネクションを確立します。 これらの接続を介してデータのパケットを送信することで通信を行います。 SSLセッションを確立するオーバーヘッドを減らすために、これらのコネクションは再利用され、比較的長寿命です。

 各パケットにはデータのサイズと種類を示すシンプルなヘッダがあります。 ノードに受信されると、ノードはパケットのタイプをチェックし、適切なハンドラを探します。 ブロック・パケットを受信した場合、ハンドラーはまずパケットを検査してすべてのブロックの先頭を見つけ、すべてのブロックがパケットのペイロードに完全に含まれていることを確認します。 すべてのチェックが通れば、ブロックはブロックディスラプターに転送されます。

 Catapult では、LMAX ディスラプタパターンのカスタム実装を使用して、主要な処理パイプラインを実装しています。 ブロック、トランザクション、そして該当する場合は部分的なトランザクションに対して、個別のディスラプターが存在します。 すべてのディスラプタは、いくつかのステージ、つまりコンシューマから構成されるという点で、同様に動作します。 ペイロードを受け取ると、ディスラプターはそれを拒否するか、最終的に受け入れるまでコンシューマーを通してそれをプッシュします。 もちろん、それぞれのディスラプターを構成するコンシューマーは異なります。

 ディスラプターパターンの主な利点のひとつは、本質的にパラレルであることを理解することが重要です。 1つのペイロードは一度に1つのコンシューマによってのみ処理され、コンシューマを順次通過していきますが、コンシューマ自体は並列に実行されます。 例えば、コンシューマ1がアイテム11を処理するのと同時に、コンシューマ3がアイテム4を処理することができます。 処理の現在の状態は、円形リングバッファで追跡されます。

 処理中、Catapultはトランザクション定義とルール(バリデータとオブザーバーの形で)をプラグインからロードします。


 具体的な理解のために、ブロック・ディスパッチャがブロックを処理するために使用するコンシューマのいくつかを見てみましょう。 ほとんどのコンシューマーは非常にシンプルで、理解しやすくテストしやすいことに気づくと思います。 さらに、ブロックディスパッチャで使用されるコンシューマーの多くは、トランザクションディスパッチャでも同様に使用されます。

 オプションのAuditConsumerは、処理中のブロックをいくつかのメタデータとともに特別な監査フォルダに書き込み、 このコンシューマーはデフォルトでは無効になっていますが、問題のデバッグに使用することができます。

 BlockHashCalculatorConsumer は、すべての依存ハッシュを計算します。 これには、すべてのトランザクションとブロックのハッシュ、およびブロックの Merkle ハッシュが含まれます。 NEMクライアントのように処理中にハッシュを何度も再計算する必要はなく、ハッシュを前もって一度だけ最適に計算します。

 BlockHashCheckConsumer は既に受信した単一ブロックを拒否します。 ネットワーク同期の一環として、ノードは複数のノードから新たに収穫されたブロックを受信することが可能です。 そのようなブロックを複数回処理するのは無駄なので、そのような場合は先制的に中止します。

 BlockchainCheckConsumerは、受信したブロック内の整合性をチェックします。 例えば、複数のブロックを受信した場合、それらがすべて適切にリンクされていなければなりません。 これらは基本的に、ブロックのグループに適用されるステートレス検証チェックです。 これらのチェックはブロック間で行われるため、Catapultが検証や状態変更の適用に使用している通知システムは、複数のブロックからのデータを含む特別な通知を追加しなければ、実際には適用できません。 その代わりに、このコンシューマーを使用します。


 BlockStatelessValidationConsumerは、状態に依存しないすべての制約を検証します。 例えば、すべてのブロックに適切な型が設定されていることや、設定された最大値以上のトランザクションが含まれていないことをチェックします。 これらのチェックはすべて状態に依存しないので、並行して実行されます。

 BlockBatchSignatureConsumer はすべてのブロックとトランザクションの署名を検証します。 この署名検証は完全に並列化されているため、特に同期時にパフォーマンスが向上します。署名検証は通常、ブロックチェーン・システムで最もコストのかかる処理の1つだからです。

 BlockchainSyncConsumerは、すべての状態に依存する制約を検証し、実際にブロックチェーンの状態を更新します。 重要なのは、これがブロックチェーンの状態へのアクセスを必要とする唯一のコンシューマーであるということです。 当然のことながら、これは最も高価なコンシューマーであり、ほとんどの場合、処理のボトルネックになります。 そのため、以下のコンシューマーは、その責任を軽減し、可能な限りスリムに保つために分割されます。

 オプションの BlockchainSyncCleanupConsumer は、ブローカー・プロセスが存在しないノードで追加のクリーンアップを実行します。 リカバリーの目的で、Catapultクライアントは基本的に、変更を適用する前にチェックポイントをディスクに書き込みます。 これにより、フォールトが発生した場合にそのチェックポイントに戻すことができます。 ブローカー・プロセスが存在する場合、これらのチェックポイント・ファイルを自動的にクリーンアップします。 そうでない場合は、このコンシューマーが行います。

 NewBlockConsumerは新しいチェーンヘッドを他のノードにプッシュします。 プッシュされるのは個々のブロックだけで、ノードがプルしなければならないブロックのグループはプッシュされないことに注意してください。 これは、最初の同期後に最もよくある状況である、新しいヘッド・ブロックの伝搬を早めるのに役立ちます。


 注意深く聞いていれば、部分トランザクションのディスラプターが常に有効になっているわけではないことに気づいたかもしれません。 これは、Catapultのコンフィギュレーションでpartialtransactionエクステンションが有効になっている場合にのみ、そのディスラプターが追加されるからです。

 そこで、エクステンションとは何かという疑問が生じます。 実際、エクステンションはCatapultの2つの主要な拡張ポイントのうちの1つです。 もう一つはプラグインで、これについては後で説明します。

 エクステンションはクライアントの機能をカスタマイズします。 拡張機能は、コンセンサス・ルールの変更以外なら何でもできます。 ネットワーク内のノードは、異なるエクステンションや同じエクステンションを異なる設定でロードすることができます。 実際、異なるタイプのノード(Peer、API、Dual)の唯一の違いは、ロードする拡張機能のセットになります。

 Catapultは基本的に、プラグインやエクステンションをロードし、それらのサービスをインスタンス化して実行させるアプリケーションシェルに過ぎません。 例えば、データ・ディレクトリを準備したり、ブート時に保存されたデータのロードを管理することをいいます。 ほぼすべての機能はプラグインとエクステンションにあります。 他の2つのディスラプター(ブロックとトランザクション)さえも、エクステンション(syncエクステンション)を介して登録されています。 このエクステンションには特別なものは何もありません。 他のエクステンションと同じ制約と機能を持っています。 ブロックとトランザクションの処理をエクステンションで実装できるという事実は、エクステンション・モデルのパワーを示しているはずだからです。

 もう少し掘り下げると、実際には2種類の拡張機能があります。 違いは、どのように通信するかという点にあります。

 最初の拡張モジュールは、単純にイベントサブスクリプション経由でデータを受け取り、それを使って何かを行います。 例えば、mongoエクステンションはこのデータを受け取り、NoSQLデータベースにコミットします。 このモデルはシンプルで柔軟性が高いので、どんな種類のデータベースにもデータを保存できます。


 確かにノードのプロセスで直接mongoエクステンションをロードすることは可能ですが、この方法にはいくつかの欠点があります。 mongoエクステンションやmongo自体にバグがあれば、ノードがダウンしてネットワークが弱体化する可能性があります。 その代わり、mongoエクステンションはコンセンサスに参加するのではなく、単にイベントを読むだけなので、別のプロセスでホストし、メインのノードプロセスから切り離すことができます。 このセットアップを使えば、バグがあってもこの別プロセスがダウンするだけで、ノードはまだ動いている状態です。 実際、このプロセスの分離はすでにサポートされており、ブローカー・プロセスを使って立ち上げ時から使用されているのです!

 もう1つのエクステンションは、サーバーフックを使ってデータをプッシュします。 例として、syncエクステンションに登録されているBlockDisruptorにどのようにブロックが移動されるかを見てみましょう。 sync エクステンションはさらに、ブロックを受け取って BlockDisruptor に転送するサーバーフックを登録します。 ブロックを処理する必要があるエクステンションは、このフックにブロックを渡すだけです。


 ブロックは複数のソースやさまざまなエクステンションから来る可能性があります:


・ブロックは同期エクステンションの他のノードから取得できます。

・ブロックはsyncsourceエクステンションの他のノードから受け取ることができます。

・ブロックはharvestingエクステンションで作成したり、取得することができます。


 これらのブロックはすべて、sync エクステンションに登録されたフックを介して BlockDisruptor に渡されます。


 エクステンションについて学んだ後は、Catapultのもう一つの主要な拡張性であるプラグインについて学んでみましょう。 エクステンションとは異なり、プラグインはブロックチェーンのコンセンサスをカスタマイズします。 プラグインは、トランザクションの種類、状態の変更、カスタム検証ルールを追加することができます。 ネットワーク内のすべてのノードは、同じプラグインを同じ設定でロードしなければなりません。


 トランザクション定義を構成する部分を見てみましょう。最初に、トランザクションのバイナリレイアウトをキャットバッファースキーマで定義する必要があります。次に、トランザクションをNotificationに分解する必要があります。 notificationはCatapultで処理できる最小単位であり、複数のプラグインで再利用できます。 これは、新しい機能を追加するために必要な新しいコードの量を減らすのに役立ちます。 例えば、送信者アカウントから受信者アカウントへのトークンの移動を示すBalanceTransferNotificationがあります。 明らかに、これは送金トランザクションのコンポーネントの1つです。 しかし、これは他の場所でも使用されます。 たとえば、新しいネームスペースを登録するときにネームスペースのレンタル料を差し引くために使用されます。


 第三に、通知が有効かどうかをチェックする検証ルールを定義する必要があります。 ステートレスとステートフルの2種類のバリデーションルールがサポートされています。 ステートレスバリデータは、状態に依存しない制約をチェックすます。 例えば、署名検証やネットワーク適合性は、これらのバリデータによってチェックできます。 対照的に、ステートフルバリデータは現在のブロックチェーンの状態を必要とします。 例えば、残高送金が許可されているかどうかをチェックするには、十分な残高があることを確認するために、送金者の現在の残高を知る必要があります。

 最後に、通知による状態変化そのものを定義する必要があります。 通常の操作では、オブザーバーは現在の状態と通知を受け取り、状態を更新します。 ロールバック時には、変更された状態と同じ通知を受け取り、変更を取り消します。

 重要なのは、バリデータとオブザーバはどちらもトランザクションではなく通知を操作することです。 通知レベルでの処理の主な利点は、複合トランザクションのサポートをシームレスにすることです。 実際には、同様のトップレベルおよび組み込みトランザクション(すなわち、集約内のトランザクション)は、そのコンテナによって生成された通知を除いて、同じ通知に分解され、同じ状態変更をもたらすことを意味します。 私たちの設計では、これをほとんど無料で実現できます。


 短時間で多くのことを学ぶことができました! あなたにとって何か学びがあり、もっと知りたくなったことを願っています。 今日はハイレベルなデザインに触れただけで、雑草の中にはもっと多くの詳細があります。

もしこの講演で他に何も覚えていないのであれば、次のことを覚えておいてほしいです:



①Catapultは基本的に空のアプリケーションシェル

②プラグインは、ネットワーク・コンセンサス・ルールとトランザクション・サポートを追加する。

③拡張機能は、結合された署名の集約からファイナライゼーションまで、他のほとんどすべてを追加する。


設計上、カスタマイズの可能性はたくさんあります。 この知識と想像力を使って、新しいノード機能を追加してくれる人がいることを期待しています。 もし行き詰まったら、いつでもXやDiscord、GitHubで私たちを見つけることができます。

ご参加ありがとうございました!