見出し画像

【Jaguar's Blog 3】Catapult: A Bird’s Eye View

この記事は2023年12月19日に出された記事「Catapult: A Bird’s Eye View」をChatGPTを用いて翻訳したものです。


2023年に東京で開催されたXymposiumで仮想的に行われたトークの転写です。Symbol(およびNEM)の動機についていくつか話しています。Catapultクライアントのアーキテクチャについての概要も提供しています。

こんにちは!挨拶ですね!¡Mucho gusto!
今日は初のXYMPOSIUMに参加していただき、ありがとうございます。皆さんが楽しい時間を過ごし、たくさん学べることを願っています。NEMの本格的な開発が始まってからほぼ10年が経ちましたが、遅れてきたとは言え、やらないよりはマシですね!今日は直接お会いできなくて残念ですが、先月の3月に日本にいて、美しい国をたくさん体験できました。
このトークでは、Catapultクライアントのアーキテクチャの高レベルな概要と、それが可能にするいくつかのあまり知られていない機能についてお話しします。しかし、最初にSymbolの動機のいくつかについて簡単にお話したいと思います。



Timeline - NEM to Symbol

Symbolを理解するためには、2014年初頭、NEMが始まった時代にタイムマシンで短い旅をする必要があります。その頃はBitcoinのハイプサイクルの終わりで、多くの - 主にひどい - 代替コインが急増していました。Bitcoinはまだ主に分散化され、元の原則により密接に準拠しており、ウォールストリートのおもちゃになる前の時代でした。その時代にはNXTと呼ばれるコインがあり、賢いProof of Stakeアルゴリズムを持っていましたが、非常に有害なコミュニティと非常に集中した分配がありました。NXTコミュニティのメンバーの一人が、NEMを構築するために開発者を募集しました - 主にNXTコミュニティのメンバーからです。私たちはBitcoinとNXTの良い側面を改善し、ポジティブなコミュニティを築きたいと考えていました。これはProof of Stakeの初期の時代であり、何が可能かを見たかったのです。主に、使いやすくて楽しいものを構築したかったのです。

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


NEM Client Limitations

NEMクライアント全体はJavaで書かれていました。当時でもJavaが最善の選択ではないことはわかっていましたが、初期に貢献したいと興味を示した少数の他の開発者に対応するためにそれを選びました。最終的に彼らは去り、私たちはガベージコレクターとの戦いに巻き込まれ、ノードを数秒間フリーズさせる可能性がありました。また、その言語の冗長性と「安全性」も私たちの進捗を遅らせました。

並列化が制限されていました。単一のブロックチェーン部分内での署名の検証のみが並列化されていました。

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

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

ブロックにはさらに、Merkleトランザクションハッシュがなかったため、ブロックヘッダーだけを迅速に同期し、後でトランザクションデータを怠惰に取得することは不可能でした。


Catbuffer

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's Architecture

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

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

Catapultでは、主要な処理パイプラインを実装するためにLMAXディスラプターパターンのカスタム実装が使用されています。ブロック、トランザクション、および該当する場合は部分トランザクション用にそれぞれ個別のディスラプターがあります。すべてのディスラプターは、いくつかのステージ(またはコンシューマ)で構成されているため、操作は基本的に同じです。ペイロードが受信されると、ディスラプターはそれをコンシューマを通じて押し、最終的には拒否されるか、受け入れられるまで処理されます。各ディスラプターを構成する具体的なコンシューマはもちろん異なります。

ディスラプターパターンの主な利点の一つは、それが本質的に並列であることです。単一のペイロードは一度に1つのコンシューマによって処理され、順次コンシューマを進行しますが、コンシューマ自体は並列に実行されます。たとえば、コンシューマ1がアイテム11を処理している最中に、コンシューマ3がアイテム4を処理している可能性があります。現在の処理状態は循環リングバッファで追跡されます。

処理中、Catapultはトランザクションの定義と規則(バリデータおよびオブザーバーの形で)をプラグインから読み込みます。これについては後でこのトークで詳しく見ていきます。


Core Processing Pipeline

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

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

BlockHashCalculatorConsumerは、すべての依存するハッシュを計算します。これにはすべてのトランザクションとブロックのハッシュ、およびブロックのMerkleハッシュが含まれます。これはNEMクライアントのように処理中に複数回再計算する必要がなく、最初に楽観的にハッシュを一度計算します。

BlockHashCheckConsumerは、すでに受信された単一のブロックを拒否します。ネットワーク同期の一環として、ノードが複数のノードから新しく採掘されたブロックを受け取る可能性があります。これらのブロックを複数回処理することは無駄なので、こうした場合には予め中断します。

BlockchainCheckConsumerは、受信したブロック内での整合性を確認します。たとえば、複数のブロックが受信された場合、それらはすべて正しく互いにリンクされている必要があります。これらは基本的に、ブロックのグループに適用されるステートレスな検証チェックです。これらのチェックはブロックを横断して行われるため、Catapultが状態の変更を検証して適用するために使用する通知システムは、実際には適用されません。代わりに、このコンシューマを使用しています。

BlockStatelessValidationConsumerは、すべての状態に依存しない制約を検証します。たとえば、すべてのブロックが適切なタイプが設定されているかどうかを確認し、構成された最大数よりも多くのトランザクションを含んでいないかを確認します。これらのチェックはすべて状態に依存していないため、並列で実行されます。

BlockBatchSignatureConsumerは、すべてのブロックとトランザクションの署名を検証します。この署名の検証は完全に並列化されており、これにより特に同期中には素敵なパフォーマンス向上が得られます。なぜなら、署名の検証は通常、ブロックチェーンシステムで最も費用がかかる操作の一つだからです。

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

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

NewBlockConsumerは、新しいチェーンヘッドを他のノードにプッシュします。個々のブロックがプッシュされ、ブロックのグループはノードによってプルされる必要があることに注意してください。これにより、新しいヘッドブロックの伝播が加速し、初期同期後に最も一般的な状況に対応できます。


拡張機能(Extentions)

注意深く聞いていたら、部分トランザクションのディスラプターが常に有効になっているわけではないことに気付いたかもしれません。これは、部分トランザクション拡張がCatapultの設定で有効になっている場合にのみ、そのディスラプターが追加されるためです。

それでは、拡張機能とは何かという問いが生じますね。実際、拡張機能はCatapultの主要な2つの拡張ポイントの1つです。もう1つはプラグインで、それについては後で詳しく説明します。

拡張機能はクライアントの機能をカスタマイズします。それらは合意ルールを変更することを除いて、何でもできます。ネットワーク内のノードは、異なる拡張機能と/または異なる構成で同じ拡張機能を読み込むことがあります。実際、異なるタイプのノード - Peer、API、Dual - の唯一の違いは、それらが読み込む拡張機能のセットです。

Catapultは基本的にはプラグインと拡張機能を読み込み、それらのサービスをインスタンス化して実行するアプリケーションシェルです。少しだけそれ以上のことを行います(たとえば、データディレクトリの準備や起動時の保存データの読み込みの管理など)、しかし大したことではありません。ほぼすべての機能は、そのプラグインと拡張機能に存在します。ブロックとトランザクションの2つのディスラプター自体も、sync拡張機能を介して登録されています。この拡張機能に特別なものはありません。他のどの拡張機能と同じ制約と機能を持っています。ブロックとトランザクションの処理が拡張機能で実装できるという事実は、拡張モデルの威力を示しています。

もう少し掘り下げると、実際には2つのタイプの拡張機能があります。その違いは、それらがどのようにコミュニケートするかに関係しています。

最初のセットの拡張機能は、単にイベントの購読を通じてデータを受け取り、それに何かしらの処理を行います。たとえば、mongo拡張機能はこのデータを受け入れ、それをNoSQLデータベースにコミットします。このモデルは、データを任意の種類のデータベースに保存するなど、シンプルで柔軟なサポートを提供するのに十分です。

mongo拡張機能をノードプロセスに直接読み込むことは確かに可能ですが、このアプローチにはいくつかのデメリットがあります。mongo拡張機能またはmongo自体のいずれかにバグがあれば、ノードをダウンさせ、ネットワークを弱体化させる可能性があります。その代わりに、mongo拡張機能は合意に参加していないが単にイベントを読み取っているだけなので、それを別のプロセスにホストしてメインのノードプロセスから分離することができます。このセットアップでは、バグがこの別のプロセスをダウンさせるだけで、ノードはまだ動作します。実際、このプロセスの分離は既にサポートされており、ローンチ以来、ブローカープロセスを使用しています!

もう1つのセットの拡張機能は、サーバーフックを使用して彼ら自身の間でデータをプッシュします。例として、どのようにしてブロックがBlockDisruptorに移動するかを見てみましょう。これはsync拡張機能に登録されているものです。sync拡張機能はさらに、ブロックを受け入れてそれらをBlockDisruptorに転送するサーバーフックを登録します。便宜上、ブロックを処理する必要がある任意の拡張機能は、単にこのフックにブロックを渡すだけです。

ブロックが複数のソースやさまざまな拡張機能から来る可能性があるにもかかわらず:

  • ブロックはsync拡張機能で他のノードから取得できます。

  • ブロックはsyncsource拡張機能で他のノードから受信できます。

  • ブロックはharvesting拡張機能で作成または採取できます。

これらのすべてのブロックは、sync拡張機能で登録されたフックを介してBlockDisruptorに渡されます。


Plugins

拡張機能について学んだら、次にCatapultのもう一つの主要な拡張ポイントであるプラグインについて学びましょう。拡張機能とは異なり、プラグインはブロックチェーンの合意をカスタマイズします。トランザクションの種類を追加したり、状態の変更やカスタム検証ルールを追加したりできます。ネットワーク内のすべてのノードは、同じプラグインと同じ構成を読み込む必要があります。

トランザクション定義を構成する部分を見てみましょう。

まず最初に、トランザクションのバイナリレイアウトをcatbufferスキーマで定義する必要があります。

次に、トランザクションを通知に分解する必要があります。通知はCatapultで処理できる最小の単位であり、そのため複数のプラグインで再利用できます。これにより、新しい機能を追加するために必要な新しいコードの量が減少します。たとえば、BalanceTransferNotificationは、送信元アカウントから受信先アカウントへのトークンの移動を示します。明らかに、これはトランスファートランザクションの構成要素の1つです。しかし、これは他の場所でも使用されます。たとえば、新しいネームスペースを登録するときにネームスペースのレンタル料金を差し引くためにも使用されます。

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

最後に、通知が与えられたときの状態変更自体を定義する必要があります。通常の動作中、オブザーバは現在の状態と通知を受け取り、状態を更新します。ロールバック中には、変更された状態と同じ通知を受け取り、変更を元に戻します。

重要なのは、バリデータとオブザーバの両方がトランザクションではなく通知上で動作することに注意してください。通知レベルで処理することの主要な利点は、複合トランザクションのサポートがシームレスになることです。実際には、似たようなトップレベルと埋め込まれたトランザクション(つまり、アグリゲート内のトランザクション)は、それぞれの通知によって発生する通知を除いて、同じ状態変更をもたらします。私たちのデザインでは、これをほとんど無料で得ることができます。


Thank You!

短い時間でたくさんの情報を受け取りましたね!何か新しいことを学び、もっと知りたいと思ってくれたら嬉しいです。今日は高レベルのデザインに触れただけで、詳細にはまだまだあります。
このトークから他に覚えておいてほしいことが何もない場合は、以下を覚えておいてほしいです:

  1. Catapultは基本的には空のアプリケーションシェルです

  2. プラグイン(Plugins)はネットワークの合意ルールとトランザクションのサポートを追加します

  3. 拡張機能(Extensions)はほとんどすべての他の機能を追加します - アグリゲートボンデッド署名の集約から最終化まで

デザイン上、カスタマイズの余地がたくさんあります。皆さんがこの知識と想像力を使って新しいノードの機能を追加することを期待しています。もし行き詰まったら、いつでもXやDiscord、GitHubで私たちに会えますよ。

ご参加いただきありがとうございました。今日の残りの時間をお楽しみください!

この記事が気に入ったらサポートをしてみませんか?