Retrying Transactions

場合によっては、一見有効なトランザクションがブロックに含まれる前に削除されることがあります。 これは、RPC ノードが leaderopen in new window. へのトランザクションの再ブロードキャストに失敗したときに、ネットワークが輻輳しているときに最もよく発生します。エンドユーザーにとっては、トランザクションが完全に消えてしまったかのように見えるかもしれません。 RPC ノードには一般的な再ブロードキャスト アルゴリズムが装備されていますが、アプリケーション開発者は独自のカスタム再ブロードキャスト ロジックを開発することもできます。

概要

Fact Sheet

  • RPCノードは、一般的なアルゴリズムを使用してトランザクションを再ブロードキャストしようとします
  • アプリケーション開発者は、独自のカスタム再ブロードキャスト ロジックを実装できます
  • sendTransaction JSON-RPC メソッドの maxRetries パラメータを利用する必要があります
  • JSON-RPC メソッドのパラメーターを利用する必要があります
  • トランザクションが送信される前にプリフライト チェックを有効にしてエラーを発生させる必要があります
  • トランザクションに再署名する前に、最初のトランザクションのブロックハッシュの有効期限が切れていることを確認することが非常に重要です

The Journey of a Transaction

クライアントがトランザクションを送信する方法

Solana には mempool という概念はありません。すべてのトランザクションは、プログラムによって開始されたかエンドユーザーによって開始されたかに関係なく、リーダーに効率的にルーティングされるため、ブロックに処理できます。ランザクションをリーダーに送信するには、主に 2 つの方法があります。:

  1. RPC server経由のプロキシによる sendTransactionopen in new window JSON-RPC method
  2. TPU Clientopen in new window経由でリーダーに直接送信

エンドユーザーの大部分は、RPC サーバー経由でトランザクションを送信します。クライアントがトランザクションを送信すると、受信側の RPC ノードが現在と次のリーダーの両方にトランザクションをブロードキャストしようとします。 トランザクションがリーダーによって処理されるまで、クライアントと中継 RPC ノードが認識しているもの以外にトランザクションの記録はありません。 再ブロードキャストとリーダー転送はクライアント ソフトウェアによって完全に処理されます。

Transaction Journey

RPC ノードがトランザクションをブロードキャストする方法

RPC ノードがsendTransactionを介してトランザクションを受信した後、関連するリーダーに転送する前に、トランザクションを UDPopen in new window パケットに変換します。UDP を使用すると、バリデーターは相互に迅速に通信できますが、トランザクションの到達に関する保証はありません。

Solana のリーダー スケジュールは、すべてのepochopen in new window (~2 days)前に知られているため、RPC ノードは、そのトランザクションを現在および次のリーダーに直接ブロードキャストします。これは、トランザクションをネットワーク全体にランダムかつ広範に伝播する Ethereum などの他のゴシップ プロトコルとは対照的です。デフォルトでは、トランザクションが終了するか、トランザクションのブロックハッシュが期限切れになるまで(この記事の執筆時点で 150 ブロックまたは約 1 分 19 秒)RPC ノードは 2 秒ごとにトランザクションをリーダーに転送しようとします。 未処理の再ブロードキャスト キューのサイズが 10,000 トランザクションopen in new windowを超える場合、新しく送信されたトランザクションはドロップされます。 コマンドライン引数open in new window を用いてRPCオペレータはこの再試行ロジックのデフォルトの動作を調整し、変更が可能です。

RPC ノードがトランザクションをブロードキャストした時、リーダーのトランザクション処理ユニット (TPU)open in new windowにトランザクションの転送を施行します。TPU は、5 つの異なるフェーズでトランザクションを処理します:

TPU OverviewImage Courtesy of Jito Labs

これら 5 つのフェーズのうち、Fetch Stageはトランザクションの受信を担当します。Fetch ステージ内で、バリデーターは受信トランザクションを 3 つのポートに従って分類します:

  • tpuopen in new window トークン転送、NFT ミント、プログラム命令などの通常のトランザクションを処理します
  • tpu_voteopen in new window 投票トランザクションのみ対応します
  • tpu_forwardsopen in new window 現在のリーダーがすべてのトランザクションを処理できない場合、未処理のパケットを次のリーダーに転送します

TPU の詳細については、Jito Labs によるこの優れた記事open in new windowを参照してください。

How Transactions Get Dropped

トランザクションの行程全体で、トランザクションが意図せずにネットワークからドロップされるシナリオがいくつかあります。

Before a transaction is processed

ネットワークがトランザクションをドロップする場合、トランザクションがリーダーによって処理される前にドロップする可能性が高くなります。 これが発生する最も単純な理由は、UDPの喪失open in new windowです。ネットワークの負荷が高いときは、バリデーターが処理に必要な膨大な数のトランザクションに圧倒される可能性もあります。 バリデーターはtpu_forwardsを介して余剰トランザクションを転送するように装備されていますが、転送open in new windowできるデータ量には制限があります。 さらに、各転送はバリデーター間の 1 つのホップに制限されます。つまり、tpu_forwardsポートで受信したトランザクションは、他のバリデーターに転送されません。

トランザクションが処理される前にドロップされる可能性がある理由として、あまり知られていない 2 つの理由もあります。 トランザクションが処理される前にドロップされる可能性がある理由として、あまり知られていない 2 つの理由もあります。最初のシナリオには、RPC プール経由で送信されるトランザクションが含まれます。これにより、プール内のノードが連携する必要がある場合に問題が発生する可能性があります。この例では、トランザクションのrecentBlockhashopen in new windowがプールの高度な部分 (バックエンド A) からクエリされます。 トランザクションがプールの遅延部分 (バックエンド B) に送信されると、ノードは高度なブロックハッシュを認識せず、トランザクションを破棄します。これは、開発者が sendTransaction preflight checksopen in new windowを有効にしている場合、トランザクションの送信時に検出できます。

Dropped via RPC Pool

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

Dropped due to Minority Fork (Before Processed)

After a transaction is processed and before it is finalized

トランザクションがマイノリティ フォークからの recentBlockhashを参照する場合でも、トランザクションが処理される可能性があります。 ただし、この場合、マイノリティ フォークのリーダーによって処理されます。このリーダーが処理されたトランザクションをネットワークの残りの部分と共有しようとすると、マイノリティフォークを認識しない大多数のバリデーターとの合意に達することができません。この時点で、トランザクションはファイナライズされる前にドロップされます。

Dropped due to Minority Fork (After Processed)

Handling Dropped Transactions

RPC ノードはトランザクションの再ブロードキャストを試みますが、使用するアルゴリズムは一般的であり、特定のアプリケーションのニーズには適さないことがよくあります。ネットワークの輻輳に備えて、アプリケーション開発者は独自の再ブロードキャスト ロジックをカスタマイズする必要があります。

An In-Depth Look at sendTransaction

トランザクションの送信に関しては、 sendTransactionRPC メソッドが開発者が利用できる主要なツールです。sendTransactionは、クライアントから RPC ノードへのトランザクションの中継のみを担当します。ノードがトランザクションを受信すると、sendTransaction は、トランザクションの追跡に使用できるトランザクション ID を返します。正常な応答は、トランザクションがクラスターによって処理またはファイナライズされるかどうかを示すものではありません。

TIP

Request Parameters

  • transaction: string - エンコードされた文字列としての完全に署名されたトランザクション
  • (optional) configuration object: object
    • skipPreflight: boolean - true の場合、プリフライト トランザクション チェックをスキップします (default: false)
    • (optional) preflightCommitment: string - Commitmentopen in new window バンク スロットに対するプリフライト シミュレーションに使用する新しいウィンドウ レベルでのコミットメントオープン (default: "finalized")。
    • (optional) encoding: string - トランザクション データに使用されるエンコーディング。 "base58" (slow)または"base64"(default: "base58").
    • (optional) maxRetries: usize - RPC ノードがリーダーへのトランザクションの送信を再試行する最大回数。このパラメーターが指定されていない場合、RPC ノードは、トランザクションが終了するか、ブロックハッシュの有効期限が切れるまで、トランザクションを再試行します。

Response

  • transaction id: string - base-58でエンコードされた文字列として、トランザクションに埋め込まれた最初のトランザクション署名。このトランザクション ID を getSignatureStatusesopen in new window で使用して、ステータスの更新をポーリングできます。

Customizing Rebroadcast Logic

独自の再ブロードキャスト ロジックを開発するには、開発者はsendTransactionmaxRetriesパラメータを利用する必要があります。指定された場合、maxRetries は RPC ノードのデフォルトの再試行ロジックをオーバーライドし、開発者が妥当な範囲内でopen in new window再試行プロセスを手動で制御できるようにします。

トランザクションを手動で再試行する一般的なパターンには、 getLatestBlockhashopen in new windowから取得したlastValidBlockHeight を一時的に保存することが含まれます。一旦隠蔽されると、アプリケーションはクラスタのpoll the cluster’s blockheightopen in new windowをポーリングし、適切な間隔で手動でトランザクションを再試行できます。 ネットワークが混雑している場合は、 maxRetriesを0に設定し、カスタム アルゴリズムを介して手動で再ブロードキャストすることをお勧めします。 一部のアプリケーションでは、exponential backoffopen in new window アルゴリズムで指数バックオフを使用する場合がありますが、Mangoopen in new window などの他のものは、一定の間隔で継続的にopen in new window トランザクションを再送信することを選択します。

Press </> button to view full source
import {
  Keypair,
  Connection,
  LAMPORTS_PER_SOL,
  SystemProgram,
  Transaction,
} from "@solana/web3.js";
import * as nacl from "tweetnacl";

const sleep = async (ms: number) => {
  return new Promise((r) => setTimeout(r, ms));
};

(async () => {
  const payer = Keypair.generate();
  const toAccount = Keypair.generate().publicKey;

  const connection = new Connection("http://127.0.0.1:8899", "confirmed");

  const airdropSignature = await connection.requestAirdrop(
    payer.publicKey,
    LAMPORTS_PER_SOL
  );

  await connection.confirmTransaction(airdropSignature);

  const blockhashResponse = await connection.getLatestBlockhashAndContext();
  const lastValidBlockHeight = blockhashResponse.context.slot + 150;

  const transaction = new Transaction({
    feePayer: payer.publicKey,
    blockhash: blockhashResponse.value.blockhash,
    lastValidBlockHeight: lastValidBlockHeight,
  }).add(
    SystemProgram.transfer({
      fromPubkey: payer.publicKey,
      toPubkey: toAccount,
      lamports: 1000000,
    })
  );
  const message = transaction.serializeMessage();
  const signature = nacl.sign.detached(message, payer.secretKey);
  transaction.addSignature(payer.publicKey, Buffer.from(signature));
  const rawTransaction = transaction.serialize();
  let blockheight = await connection.getBlockHeight();

  while (blockheight < lastValidBlockHeight) {
    connection.sendRawTransaction(rawTransaction, {
      skipPreflight: true,
    });
    await sleep(500);
    blockheight = await connection.getBlockHeight();
  }
})();

getLatestBlockhashを介してポーリングする場合、 アプリケーションは意図したcommitmentopen in new windowレベルで開くように指定する必要があります。 コミットメントを confirmed (voted on) または finalized (~30 blocks after confirmed)に設定することで、アプリケーションは、マイノリティ フォークからのブロックハッシュのポーリングを回避できます。

アプリケーションがロード バランサーの背後にある RPC ノードにアクセスできる場合、そのワークロードを特定のノードに分割することも選択できます。getProgramAccounts などのデータ集約型のリクエストを処理する RPC ノードは、処理が遅れる傾向があり、トランザクションの転送にも適していない可能性があります。時間に敏感なトランザクションを処理するアプリケーションの場合、sendTransactionのみを処理する専用ノードを用意するのが賢明な場合があります。

The Cost of Skipping Preflight

デフォルトでは、sendTransactionは、トランザクションを送信する前に 3 つのプリフライト チェックを実行します。具体的には次のことを行います:

  • すべての署名が有効であることを確認する
  • 参照されたブロックハッシュが最後の 150 ブロック内にあることを確認します
  • preflightCommitmentで指定された銀行スロットに対してトランザクションをシミュレートします。

これら 3 つのプリフライト チェックのいずれかが失敗した場合、 sendTransaction はトランザクションを送信する前にエラーを発生させます。多くの場合、プリフライト チェックは、トランザクションを失うことと、クライアントがエラーを適切に処理できるようにすることの違いになる可能性があります。これらの一般的なエラーが確実に説明されるようにするために、開発者はskipPreflightfalseに設定しておくことをお勧めします。

When to Re-Sign Transactions

再ブロードキャストのあらゆる試みにもかかわらず、クライアントがトランザクションに再署名する必要がある場合があります。ランザクションに再署名する前に、最初のトランザクションのブロックハッシュの有効期限が切れていることを確認することが非常に重要です。最初のブロックハッシュがまだ有効な場合、両方のトランザクションがネットワークによって受け入れられる可能性があります。エンドユーザーには、同じトランザクションを意図せずに2回送信したように見えます。

Solana,では、削除されたトランザクションは、参照するブロックハッシュが getLatestBlockhash から受け取ったlastValidBlockHeightよりも古い場合、安全に破棄できます。 開発者は、getEpochInfoopen in new window をクエリし、応答のblockHeightと比較して、この lastValidBlockHeight を追跡する必要があります。ブロックハッシュが無効になると、クライアントは新しくクエリされたブロックハッシュで再署名できます。

Acknowledgements

Trent Nelson、 Jacob Creechopen in new window、White Tiger、 Le Yafo、 Buffaluopen in new windowJito Labsopen in new windowのレビューとフィードバックに感謝します。

Last Updated:
Contributors: PokoPoko2ry