新着:HeliusがLight Protocolを買収
Solanaでトランザクションを確実にランディングさせる方法
ブログ/開発

Solanaでトランザクションを確実にランディングさせる方法

デベロッパーエクスペリエンスエンジニアXのAnam AnsariLinkedInのAnam Ansari
読了時間:18分

最近、Solanaではかつてない規模のトラフィックが発生しており、トランザクションの失敗やドロップ率が高まっています。

Solanaの1秒あたりのトランザクション数(TPS)は、投票以外のトランザクションで約1,000以上です。Quinn(ネットワークレイヤーであるQUICのRust実装)には、需要が高い状況でスパムを効率的に処理するうえで制約があり、ブロックリーダーが接続を選択的に切断せざるを得なくなる場合があります。失敗した全トランザクションのうち、実際のユーザーが開始したものは約8%で、残りはボットによる任意のトランザクションでした。

失敗したトランザクションに対処するには、Solanaでトランザクションがどのように送信され、処理されるかを理解することが不可欠です。この記事では、トランザクションが失敗する原因を掘り下げ、トランザクションのスループットを高めるためのベストプラクティスを紹介します。この記事は、Solanaのプログラミングモデル、およびトランザクションの作成と送信について基本的な知識があることを前提としています。

トランザクション

プログラムの実行は、クラスターに送信されたトランザクションから始まります。トランザクションには以下が含まれます。

  • 読み取りまたは書き込みを行うすべてのアカウントの配列
  • 1つ以上の命令(最小の実行単位)
  • 最新のブロックハッシュ
  • 1つ以上の署名

ランタイムは、トランザクションに含まれる各命令を順番にアトミックに処理します。命令の一部でも失敗すると、トランザクション全体が失敗します。

ブロックハッシュとは?

「ブロックハッシュ」とは、スロットの最新のProof of History(PoH)ハッシュです。Solanaは信頼できるクロックとしてPoHを使用するため、トランザクションの最新のブロックハッシュはタイムスタンプとみなせます。ブロックハッシュは重複を防ぎ、トランザクションに有効期間を与えます。トランザクションのブロックハッシュが古すぎる場合、そのトランザクションは拒否されます。ブロックハッシュの最大有効期間は150ブロック、つまり~1分19秒です。

トランザクションはどのように送信されますか?

Solanaは、台帳に追加されるトランザクションを検証するバリデーターのグループによって維持されています。このグループからリーダーバリデーターが選ばれ、台帳にエントリを追加します。台帳上のエントリは、ティックまたはトランザクションのエントリです。台帳には、クライアントが署名したトランザクションを含むエントリの一覧が保持されます。概念上、台帳はジェネシスブロックまで遡れます。ただし、将来のブロックの検証には古いブロックが不要となる設計のため、実際のバリデーターの台帳では、ストレージを節約するために新しいブロックのみを保持する場合があります。

リーダーバリデーターが生成できるブロックは、スロットごとに1つだけです。ブロックハッシュは各ブロックを識別する一意の識別子です。これは、直前のブロックのハッシュを含む、ブロック内の全エントリのハッシュです。任意の時点でどのバリデーターが現在のリーダーになるかを決めるリーダースケジュールは、各エポックの前、通常は約2日前に決定されます。トランザクションが開始されると、現在および次のリーダーバリデーターに転送されます。

トランザクションは、次の方法でリーダーに送信できます。 

  1. RPCサーバー:RPCプロバイダーは、sendTransaction JSON-RPCメソッドを使用してトランザクションを送信できます。受信したRPCノードは、トランザクションがファイナライズされるか、トランザクションのブロックハッシュが期限切れになるまで(150ブロック後、または~1分19秒後)、2秒ごとに現在および次のリーダーへUDPパケットとして送信を試みます。それまでは、クライアントと中継するRPCノードが把握している情報以外に、トランザクションの記録はありません。 
  2. TPUクライアント:TPUクライアントは、単にトランザクションを送信します。再ブロードキャストとリーダーへの転送は、クライアントソフトウェアで処理する必要があります。

sendTransactionメソッドを使用するには、文字列としてエンコードしたトランザクションオブジェクトを渡す必要があります。その他のオプションパラメーターは次のとおりです。

  1. encoding:トランザクションデータに使用するエンコーディングはbase58またはbase64です。 
  2. skipPreflight:プリフライトチェックでは、トランザクション署名の検証と、プリフライトのコミットメントで指定されたバンクスロットに対するトランザクションのシミュレーションを行います。プリフライトチェックに失敗すると、エラーが返されます。この機能のデフォルト設定はfalseであり、プリフライトチェックをスキップしないことを意味します。
  3. preflightCommitment:プリフライトチェックの実行時に使用するコミットメントレベルを指定します。コミットメントレベルはデフォルトでfinalizedに設定されていますが、文字列を指定して変更できます。わかりにくい挙動を避けるため、コミットメントとプリフライトのコミットメントには同じ値を指定することを推奨します。
  4. maxRetries:maxRetriesパラメーターは、RPCノードがリーダーへのトランザクション送信を再試行する最大回数を決定します。このパラメーターを指定しない場合、RPCノードはトランザクションがファイナライズされるか、ブロックハッシュが期限切れになるまで再試行します。 
  5. minContextSlot:minContextSlotパラメーターは、トランザクションのプリフライトチェックを実行する最小スロットを指定します。

トランザクションはどのように処理されますか?

バリデーターのTransaction Processing Unit(TPU)はトランザクションを受信し、署名を検証して実行した後、ネットワーク内の他のバリデーターと共有します。

TPUは、トランザクションを5つの異なるフェーズで処理します。

フェッチステージ

フェッチステージは、トランザクションの受信を担当します。受信したトランザクションを3つのポートに分類します。

  • tpu:トークン転送、NFTのミント、プログラム命令などの通常のトランザクションを処理します
  • tpu_vote:投票トランザクションのみを処理します
  • tpu_forwards:現在のリーダーがすべてのトランザクションを処理できない場合、未処理のパケットを次のリーダーへ転送します

パケットは128個ずつのグループにまとめられ、SigVerifyステージへ転送されます。

SigVerifyステージ

SigVerifyステージは、パケットの署名を検証し、検証に失敗したパケットを除外します。投票パケットと通常のパケットは、2つの別々のパイプラインで処理されます。ソフトウェアの観点では、受信したパケットにはメタデータが含まれていますが、この段階ではそれらがトランザクションかどうかはまだ判別できません。

GPUが搭載されている場合、署名検証に使用されます。また、トラフィックが増加してパケットが過剰になった場合に、IPアドレスを利用してパケットをドロップするロジックもあります。

バンキングステージ

このステージは、トランザクションのフィルタリングと処理を担当します。現在は6つの独立したワーカースレッドで構成され、そのうち2つが投票スレッド、4つが非投票スレッドです。通常のトランザクションは非投票スレッドに追加されます。各スレッドには、競合しないトランザクションを優先度キューに最大64件保持できるローカルバッファがあります。これらのトランザクションは、Sealevelによって並列処理されます。バンキングステージの詳細については、こちらの動画をご覧ください。

Proof of Historyサービス

PoHサービスモジュールは、ティックの経過を記録します。各ティックは時間の単位を表し、1スロットには64ティックあります。バンキングステージからレコードを受信するまで、ハッシュが繰り返し生成されます。

next_hash = hash(prev_hash, hash(transaction_ids))

これらのレコードはエントリに変換され、ブロードキャストステージを介してネットワークへ配信されます。

ブロードキャストステージ

PoHサービスからのエントリは、ブロックの最小単位を表すシュレッドに変換され、Turbineと呼ばれるブロック伝播技術を使用してネットワークの残りの部分へ送信されます。大まかに言うと、Turbineはブロックを小さな断片に分割し、階層構造のノードを通じて配信します。各ノードが他のすべてのノードと接続する必要はなく、選ばれた少数のノードとのみ通信すれば済みます。Turbineとその仕組みの詳細については、こちらの記事をご覧ください。 

トランザクションが失敗するのはなぜですか?

不正な命令やカスタムプログラムのエラーによる失敗を除くと、トランザクションが失敗する原因として次のものが考えられます。

ネットワークでのドロップ

ネットワークレイヤーでは、リーダーが処理する前にトランザクションがドロップされる可能性があります。最も単純な原因はUDPパケットロスです。もう1つの原因は、TPUのfetch stageに関連しています。ネットワークの負荷が高いと、バリデーターが処理すべきトランザクション数に圧倒される場合があります。バリデーターは、余分なトランザクションを次のバリデーターのtpu_forwardポートへ転送できます。ただし、転送できるデータ量には上限があり、各転送はバリデーター間の1ホップに制限されます。つまり、tpu_forwardsポートで受信したトランザクションは、他のバリデーターへ転送されません。保留中の再ブロードキャストキューが10,000トランザクションを超えると、新たに送信されたトランザクションはドロップされます。

古い/不正なブロックハッシュ 

すべてのトランザクションには、Proof of History(PoH)クロックのタイムスタンプとして機能する「最新のブロックハッシュ」があります。このブロックハッシュにより、バリデーターは同じトランザクションを二重に処理することを防ぎ、トランザクションがいつ、どの順序で処理されたかを追跡できます。処理中にブロックハッシュが無効と判定されると、バリデーターはトランザクションを拒否します。

ブロックハッシュの期限切れ

トランザクションのブロックハッシュは、十分に「新しい」とみなされなくなると期限切れになります。トランザクションを処理するため、Solanaのバリデーターはブロック内で対応するブロックハッシュのスロット番号を検索します。バリデーターがそのブロックハッシュのスロット番号を見つけられない場合、または検索されたスロット番号が処理中のブロックのスロット番号より151スロット以上前の場合、トランザクションは拒否されます。デフォルトでは、Solanaのトランザクションは一定時間(~*1分19秒)*以内にブロックへコミットされないと期限切れになります。

遅延しているRPCノード

RPC経由でトランザクションを送信する場合、そのRPCプールが他より先行していることがあります。これにより、プール内のノードが連携する際に問題が発生する可能性があります。たとえば、トランザクションのrecentBlockhashをプール内の先行している部分から取得し、遅延している部分へ送信すると、ノードは先行したブロックハッシュを認識できず、トランザクションを拒否します。sendTransactionでプリフライトチェックを有効にすると、トランザクション送信時にこの問題を検出できます。

一時的なネットワークフォーク

一時的なネットワークフォークによって、トランザクションがドロップされることもあります。バリデーターがバンキングステージ内でブロックを再生するのに時間がかかると、少数派フォークが作成される場合があります。クライアントがトランザクションを構築する際、そのトランザクションが少数派フォークにのみ存在するrecentBlockhashを参照する可能性があります。トランザクションの送信後、処理される前にクラスターが少数派フォークから切り替わる場合があります。このシナリオではブロックハッシュが見つからないため、トランザクションがドロップされます。

トランザクションを確実にランディングさせるには?

確認に関する問題を診断するには、トランザクションの有効期限を理解することが重要です。トランザクションの成功率を高めるには、次の手順に従ってください。

要点

  • コミットメント「confirmed」または「finalized」で最新のブロックハッシュを取得します
  • skipPreflightをtrueに設定します
  • リクエストするCompute Unitsの量を最適化します
  • 優先手数料を追加し、動的に計算します
  • maxRetriesを0に設定し、トランザクション送信のカスタム再試行ロジックを追加します。
  • ステーク接続を検討します
  • トランザクションに時間的制約がない場合は、Durable Nonceを使用します

ブロックハッシュ

トランザクションをバリデーターが処理できる時間には制限があります。バリデーターが処理する前にトランザクションに関連付けられたブロックハッシュが期限切れになると、トランザクションはキャンセルされます。トランザクションを確実に通すには、最新のブロックハッシュを使用して送信することが重要です。バリデーターがトランザクションを処理する前にブロックハッシュが期限切れになった場合は、新しいブロックハッシュを使用して再試行できます。これにより、正常に処理される可能性が高まります。これには2つの方法があります。 

1. 新しいコミットメントレベルを設定する:

最新のブロックハッシュを取得するために推奨されるRPC APIメソッドは、getLatestBlockhashです。デフォルトでは、このメソッドはfinalizedコミットメントレベルを使用し、直近でファイナライズされたブロックのブロックハッシュを返します。このコミットメントレベルは、そのブロックの上に少なくとも31個の確認済みブロックが追加されていることを示します。これにより、ドロップされたフォークに属するブロックハッシュを使用するリスクがなくなります。ただし、直近の確認済みブロックとファイナライズ済みブロックの間には通常、少なくとも32スロットの差があります。このトレードオフにより、トランザクションの有効期間は約13秒短くなり、クラスターが不安定な状況ではさらに短くなる可能性があります。 

コミットメントパラメーターを別のレベルに設定すると、ブロックハッシュのコミットメントを上書きできます。confirmedコミットメントレベルは、通常processedコミットメントレベルから数スロットしか遅れておらず、ドロップされたフォークに属する可能性も低いため、RPCリクエストに推奨されます。processedコミットメントレベルは、他のコミットメントレベルより新しいブロックハッシュを取得できますが、推奨されません。Solanaプロトコルのフォークにより、約5%のブロックがクラスターによってファイナライズされないためです。トランザクションでドロップされたフォークに属するブロックハッシュを使用すると、ファイナライズ済みブロックチェーン内のどのブロックからも最新とはみなされません。

2. 新しい最新ブロックハッシュを頻繁にポーリングする:

getLatestBlockhashメソッドを使用し、最新のブロックハッシュを頻繁に(60秒ごとに)取得して保存するスクリプトを追加します。これにより、ユーザーがトランザクションを開始したとき、アプリケーションには新しいブロックハッシュが用意されています。ウォレットも新しいブロックハッシュを頻繁にポーリングし、トランザクションへ署名する直前に最新のブロックハッシュを置き換えて、可能な限り新しい状態にする必要があります。

プリフライトのスキップ

トランザクションを送信する前に、次のプリフライトチェックが実行されます。

  • トランザクションの署名を検証します。
  • プリフライトのコミットメントで指定されたバンクスロットに対してトランザクションをシミュレーションします。失敗した場合はエラーが返されます。 

シミュレーション用に選択されたブロックが、トランザクションのブロックハッシュに使用したブロックより古い場合、恐ろしい「blockhash not found」エラーが発生してシミュレーションが失敗します。 

トランザクション署名が検証済みで、ほかにエラーがないと確信できる場合は、プリフライトチェックをスキップできます。skipPreflightパラメーターを使用する場合でも、sendTransactionとsimulateTransactionの両方のリクエストで、preflightCommitmentパラメーターをトランザクションのブロックハッシュ取得時と同じコミットメントレベルに必ず設定してください。

Compute Units

ネットワーク上でトランザクションが確認されると、ブロック内で利用可能なCompute Units(CU)の一部が使用されます。現在、ブロック全体のCompute上限は48M CUです。開発者はトランザクションのCompute Unit予算を指定できます。予算を設定しない場合は、デフォルト値の200,000が使用されます。必要以上に高い予算をリクエストしてもペナルティがないため、多くのトランザクションはCU予算のすべてを使用しません。しかし、スケジューラーはトランザクションが実行されるまでブロック内に残っているCompute量を把握できないため、あらかじめ多すぎるCompute Unitsをリクエストすると、トランザクションを効率的にスケジュールしにくくなります。これを避けるため、開発者はトランザクションの要件に合った、より適切な範囲のCUリクエストを設定する必要があります。Compute Unit予算を最適化する方法については、こちらのガイドをご覧ください。今後のSolanaクライアントv1.18アップデートでは、必要なCompute Unitsが少ないトランザクションほど優先度が高くなります。

Compute Unit(CU)の使用量を最適化すると、次のメリットがあります。

  • 小さいトランザクションほどブロックに含まれやすくなります。
  • コストの低い命令によって、プログラムのコンポーザビリティが高まります。
  • ブロック全体の使用量が減り、より多くのトランザクションをブロックに含められます。

優先手数料を実装する

基本トランザクション手数料に優先手数料を上乗せすると、バリデーターによるトランザクションの優先度を高められます。この手数料は、Compute UnitあたりのマイクロLamport(少額のSOLなど)で設定されます。トランザクションに追加することで、バリデーターノードがネットワーク上のブロックに含める経済的なメリットが生まれます。

ただし、優先手数料として支払うべき金額には上限がある点に注意が必要です。一般的な手数料を超えて支払っても、トランザクションが成功する確率は上がりません。そのため、競争力を維持しながら過払いを避けられるよう、優先手数料を動的に計算することを推奨します。統合は簡単です。優先手数料に関する公式ドキュメントを参照するか、すぐに利用できるHelius APIをご利用ください。

堅牢な再試行ロジックを実装する

ネットワークが混雑した場合に備え、トランザクションの失敗を処理し、手動で再試行するカスタムロジックをコードに実装してください。sendTransactionを使用してトランザクションを送信する際は、maxRetriesパラメーターを0に設定します。トランザクションを再試行する方法はいくつかあります。

  • 異なるコミットメントレベルでtransaction statusをポーリングし、スパムを避けるため指数バックオフを使用しながら、確認されるまで同じ署名済みトランザクションを継続して使用します。または、タイムアウトするまで一定の間隔でトランザクションを送信できます。
  • getLatestBlockhash methodから返されるlastValidBlockHeightを保存します。その後、クラスターのブロック高をポーリングし、現在のブロック高がlastValidBlockHeightを超えたら、トランザクションを手動で再試行します。getLatestBlockhashでポーリングする際は、目的のコミットメントレベルを指定することを推奨します。コミットメントをconfirmed(投票済み)またはfinalized(confirmedから~30ブロック後)に設定すると、少数派フォークからブロックハッシュをポーリングすることを回避できます。

ステーク接続

リーダーのネットワーク帯域幅には限りがあります。送信元を考慮せず、先着順ですべてのトランザクションを無条件に受け入れることを避け、帯域幅を効果的に使用するには、ステークによる重み付けが必要です。SolanaはProof of Stakeネットワークとして運用されているため、ステークによる重み付けの用途を拡張し、トランザクションのサービス品質を改善するのは自然な流れです。つまり、0.5%のステークを持つノードは、少なくとも0.5%のパケットをリーダーへ送信できます。残りのネットワークは、残りのステークをどのように組み合わせても、それらを完全に締め出すことはできません。この仕組みはStake-Weighted Quality of Service(SWQoS)と呼ばれます。

Heliusでは、有料プラン向けにステーク接続を提供しています。詳しくは、ドキュメントのSolanaでのトランザクション送信をご覧ください。

Durable Nonce

Durable Nonceを使用すると、将来の任意の時点で送信できるトランザクションを作成し、署名できます。トランザクション署名の生成に長い時間を必要とするカストディサービスなどのケースで使用されます。トランザクションに時間的制約がない場合、この方法を使用して、トランザクションのrecentBlockhashの短い有効期間を回避できます。

Durable Transactionの使用を開始するには、オンチェーンに特別な「nonce」アカウントを作成し、その内部に「durable blockhash」を保存する命令を呼び出すトランザクションを送信する必要があります。Nonceアカウントにはnonceの値が保存されます。Nonceアカウントがまだ使用されていない限り、次の2つのルールに従ってDurable Transactionを作成できます。

  • 命令の一覧は、オンチェーンのnonceアカウントを読み込む「advance nonce」システム命令から始める必要があります。
  • トランザクションのブロックハッシュは、オンチェーンのnonceアカウントに保存されたdurable blockhashと一致する必要があります。

CLIとWeb3.jsを介してDurable Nonceを実装する方法については、こちらの記事をご覧ください。

トランザクション送信に対するHeliusのアプローチ

sendTransactionリクエストは、最寄りのRPCノードへ自動的にルーティングされます。maxRetriesが指定されていない場合、ブロックハッシュが期限切れになるまで2秒ごとにトランザクションを再試行します。maxRetriesを0に設定し、確認されるまで2秒ごとにトランザクションをご自身で再ブロードキャストすることを推奨します。 

現在のネットワーク混雑に対処するため、ユーザーのトランザクションランディング率の改善に24時間体制で取り組んでいます。sendTransactionリクエストのレート制限を引き下げました。この措置は、混雑を管理し、バリデーターへのスパムを防止するためのものです。制限はこちらで確認できます。

次に、有料プランの高品質なトラフィックをステーク接続経由でルーティングしています。これらのステーク接続には、当社のバリデーターを活用しています。優先手数料の合計が少なくとも10,000 Lamports(クラスターの中央値)であれば、高品質なトラフィックとみなされます。

バリデーターに高品質なトラフィックを提供するため、手数料の合計が10,000 Lamportsを超えることを要件としています。バリデーターは、手数料の低いトランザクションを送信するトラフィックソースに対し、レート制限を適用し始めており、完全にブロックする場合もあります。

ステーク接続を使用すると、トランザクションのランディング率を大幅に改善できます。トランザクション作成時の優先手数料の設定方法については、こちらの記事をご覧ください。

推奨事項

初級/中級ユーザー

skipPreflightをfalseに設定することを推奨します。プリフライトチェックでは、トランザクション署名の検証と、プリフライトのコミットメントで指定されたバンクスロットに対するトランザクションのシミュレーションを行います。プリフライトチェックに失敗すると、エラーが返されます。プリフライトチェックを実施しないと、設定ミスによりトランザクションがドロップされる可能性があります。

上級ユーザー

可能な限り低いレイテンシを必要とする上級ユーザーは、skipPreflightをtrueに設定してください。ただし、トランザクションが正しく構成されていることをご自身で確認する必要があります。 

まとめ

混雑時にSolanaネットワーク上でトランザクションを確実にランディングさせるには、ネットワークアーキテクチャとトランザクション処理メカニズムを詳細に理解する必要があります。トランザクションの一意性と適時性におけるブロックハッシュの役割、RPCサーバーやTPUクライアントを介したトランザクション送信プロセス、skipPreflight、preflightCommitment、maxRetriesなどの適切なパラメーターを設定する重要性といった中核概念を理解することで、トランザクションのパフォーマンスを大幅に向上できます。カスタムの再試行メカニズムを実装し、ステーク接続を活用することも、成功率の向上に役立ちます。

さらに、近日公開予定のv1.18クライアントに見られるように、ネットワークの現在の制約と、それに対処するためのAnzaの継続的な取り組みを認識することが重要です。ネットワークが進化し、スケールするなかで、最新情報を把握して柔軟に対応することが、ネットワークと効果的にやり取りする鍵となります。 

ヘルプやサポートが必要な場合は、Discordからお気軽にお問い合わせください。Solanaの最新情報を見逃さないよう、以下にメールアドレスをご入力ください。さらに詳しく知りたいですか?Heliusブログの最新記事を読み、今すぐSolanaの旅を続けましょう。

リソース

Heliusを購読

Solana開発の最新情報や新しい記事の公開通知を受け取れます

拡大画像