Solanaバリデーター入門:取引処理(21年11月23日)
本記事はJito Labsのブログ記事 Solana Validator 101: Transaction Processing(2021年11月23日)の日本語訳になります。
本記事は情報提供を目的としたものであり、投資の勧誘を目的としたものではありません。暗号資産への投資は価格変動のリスクなど多大なリスクを伴います。投資の最終決定はご自身の判断で行ってください。
Solana Validatorのシリーズ記事の第一弾です。今回は、取引が署名されていない取引から伝播されたブロックになるまでのライフサイクルを技術的に深く掘り下げます。
コードを追ってみたい場合は、コミットcd6f931223を参照してください。これは2021年11月21日時点のマスタブランチなので、バリデータはそれよりも古いバージョンのコードを実行している可能性があります。
コードベースは非常に活発なので、常にSolanaのGitHubをチェックして最新のコードを確認してください。お気に入りのIDEにロードし、記事と一緒に定義をクリックして調べてください。
Jito LabsはSolana向けのMEVインフラストラクチャを構築しています。リバースエンジニアリング、コードハッキング、または高性能システムの構築に興味があるエンジニアを募集しています!Twitterで@buffalu__にDMしてください。
さて、SerumやMangoでコインを衝動買いしているとしましょう。ウォレットで取引に署名すると、すぐに取引が確認され、通知が届きます。一体何が起こったのでしょうか?
dappは、トークン購入の取引を構築
あなたのウォレット(Phantom、Solletなど)に署名を促す取引を送信
ウォレットはあなたの秘密鍵を使用して取引に署名し、DAppに返す
dappは署名された取引をdappで指定された現在のRPCプロバイダーに送信するために、sendTransaction HTTP APIコールを使用
RPCサーバーはあなたの取引をUDPパケットとして、リーダースケジュール上の現在のリーダーと次のリーダーに送信
バリデータのTPU(取引処理ユニット)は取引を受信し、署名をCPUまたはGPUを使用して検証、実行され、ネットワーク内の他のバリデータに伝播
Solanaは、エポックごとに(約2日)生成される既知のリーダースケジュールを持っており、取引をランダムにゴシップするイーサリアムのmempoolのようにはいかず、直接現在のリーダーと次のリーダーに送信します。比較はこちらをご参照ください。
さらに詳しく見ていきましょう。
取引処理ユニット
RPCサービスは、取引をHTTPで受信し、UDPパケットに変換し、リーダースケジュールを使用して現在のリーダーの情報を調べ、リーダーのTPUに送信します。
TPU(取引処理ユニット)は、取引を処理します。メッセージキュー(Rustチャネル)を使用して、複数のステージからなるソフトウェアパイプラインを構築します。ステージnの出力がステージn+1に接続されます。
FetchStage
Solanaのバリデータは、他のブロックチェーンと異なり、UDPパケットを使用して相互に通信します。UDPはコネクションレスプロトコルであり、メッセージの順序や配信を保証しません。これにより、TCPのバックプレッシャーの問題や接続管理のオーバーヘッドを排除し、バリデータオペレータにとってサーバーとネットワークの設定をシンプルにすることができます。ただし、これにより、取引とゴシップメッセージのサイズが制限され、バリデータのDOS攻撃の可能性が高くなり、取引や他のパケットがドロップされる可能性があります。
FetchStageは、各パケットタイプ専用のソケットを持ちます:
tpu:通常の取引(Serumオーダー、NFTミント、トークン転送など)
tpu_vote:投票
tpu_forwards:現在のリーダーがすべての取引を処理できない場合、未処理のパケットをこのポートで次のリーダーに転送
パケットは128個のグループにバッチされ、SigVerifyStageに転送されます。
上記のソケットはここで作成され、ContactInfo構造体に格納されます。ContactInfoはノードに関する情報を保持し、ゴシッププロトコルを通じてネットワークの他のノードと共有されます。
SigVerifyStage
SigVerifyStageは、パケットの署名を検証し、失敗した場合は破棄するようにマークします。ソフトウェアの観点からは、これらはまだメタデータを含むパケットであり、取引かどうかはまだわかりません。
GPUがインストールされている場合は、署名検証にGPUを使用します。また、負荷が高い場合に過剰なパケットを処理するためのロジックも含まれており、IPアドレスを使用してパケットをドロップします。
投票と通常のパケットは、2つの別々のパイプラインで実行されます。投票パイプラインに到着するパケットは、単純なロジックを使用して、パケットが投票かどうかを判断し、投票パイプラインでのDoSを防止します。これは、今年初めのネットワーク障害のような事態を回避するためのもう一つの対策です。
BankingStage
これはバリデータの要であり、これまでのレビューからすると最も理解するのが難しいセクションです。
このステージには、3つのパケットタイプが送信されます:
検証されたゴシップ投票パケット
検証されたtpu_voteパケット
検証されたtpuパケット(通常の取引)
各パケットタイプには独自の処理スレッドがあり、通常の取引パイプラインには2つのスレッドがあります。
現在のノードがリーダーになると、TPU内のすべてのパケットは次のステップを通過します:
パケットをSanitizedTransactionにデシリアライズします。
取引をQoS(Quality of Service)モデルで実行します。これは、署名、命令データバイトの長さ、特定のプログラムIDのアクセスパターンに基づく何らかのコストモデルなどのいくつかのプロパティに基づいて、実行する取引を選択します。
パイプラインは次に、実行する取引のバッチを取得します。この取引のグループは、並列化可能なエントリ(並列実行できる取引のグループ)を形成するために貪欲に選択されます。これを行うために、クライアントが取引を構築するときに設定するisWriteableフラグと、アカウントごとの読み書きロックを使用して、データ競合を防止します。
取引が実行されます。
結果はPohServiceに送信され、次にブロードキャストステージに転送され、シュレッディング(パケット化)されてネットワークの他の部分に伝播されます。また、銀行とアカウントデータベースに保存されます。
処理されなかった取引は、バッファリングされ、次の反復で再試行されます。バリデータがブロックを生成する直前に取引を受け取った場合、リーダーになるまで保持します。リーダーnの時間が経過しても未処理の取引が残っている場合、未処理の取引をリーダーn+1のTPUフォワードポートに転送します。
並列処理
Solanaの刺激的な機能の1つは、プログラミングモデルのコードと状態の分離(Sealevelと呼ばれる)によって有効化された、ランタイムの取引処理を並列化できる能力です。取引命令は、読み書きするアカウントを明示的にマークする必要があります。
これは開発者にとって非常に面倒なことですが、良い理由があります。BankingStageは、並列処理で取引のバッチを実行し、競合状態に陥るのを防ぐことができるからです。
取引はバッチ処理されます。各バッチで生成されるエントリには、並列実行できる取引のリストが含まれています。したがって、他の並列パイプラインで処理されるバッチに加えて、各バッチでRWロックが必要になります。
いくつかの例を見てみましょう。実際には、取引は複数のアカウントにアクセスします。単純にするために、各ボックスは1つのアカウントにアクセスする取引であると想像してください。
上記例では、アカウントCは、2つの別々のパイプラインの2つのバッチ間で共有される唯一のアカウントです。1つのバッチはCから読み込もうとし、もう1つは書き込もうとしています。競合状態を回避するために、バリデータは読み書きロックを使用してデータ破損を防ぎます。この場合、トップパイプラインがCを最初にロックして実行を開始します。同時に、ボトムパイプラインはバッチを実行しようとしますが、Cはすでにロックされているため、Cを除くすべてを実行し、次の反復のために保存します。
この例では、アカウントCを読み書きする複数の取引があります。ブロックプロデューサーに伝播されるエントリは並列実行をサポートする必要があるため、Cへの最初のアクセスと、その後RWロックのルールに従う他の取引を取得します。
PohService
Proof of History(PoH)は、時間の経過を証明するためのSolanaの技術です。これは、検証可能な遅延関数(VDF)に似ています。詳細についてはこちら。
PohServiceは、スロット(1スロット=Nティック)よりも小さい時間の単位であるティックを生成する役割を担います。これはプロセッサコアに固定され、次のようなハッシュループを実行します:
output = "solana summer"
while 1:
output = hash([output])
ハッシュは、BankingStageからRecordが受信されるまで自身から生成されます。その時点で、現在のハッシュとmixin(バッチ内のすべての取引のハッシュ)が結合されます。これらのレコードは、BroadcastStageを介してネットワークにブロードキャストできるように、エントリに変換されます。擬似コードは次のようになります。
output = "solana summer"
record_queue = Queue()
while 1:
record = record_queue.maybe_pop()
if record:
output = hash([output, record])
else:
output = hash([output])
BroadcastStage
このステージは、PohServiceによって生成されたエントリをネットワークの他の部分にブロードキャストする役割を担います。これらのエントリは、ブロックの最小単位であるシュレッドに変換され、Turbineと呼ばれるブロック伝播技術を使用してネットワークの他の部分に送信されます。要約すると、各ノードは親ノードからブロックの部分的なビュー(最大64KBのパケットサイズ)を受け取り、それを自分の子ノードと共有します。
受信側では、バリデータはシュレッドを聞き取り、エントリに戻してからブロックに変換します。その詳細は、今後の記事で説明します。
まとめ
ご覧のとおり、バリデータは非常に複雑なエンジニアリングであり、私たちは表面をなぞっただけです。オペレーティングシステムからインスピレーションを得ており、チームがパフォーマンスを高めるために使用する多くのシステムレベルのトリックがあることがわかります。Solanaのエンジニアリングチームと貢献者に、この素晴らしいエンジニアリングに対する大きな拍手を送ります。
より民主的なMEVシステムをSolana上に構築する際の課題について、より深く掘り下げた記事をお楽しみに。
Discordでのサポートに感謝します。読んでいただきありがとうございます。@jito_network、@buffalu__、@segfaultdoctorをフォローして、今後の記事にご期待ください。
この内容に興味がある場合は、ぜひ@buffalu__までご連絡ください。