Retrying Transactions
場合によっては、一見有効なトランザクションがブロックに含まれる前に削除されることがあります。 これは、RPC ノードが leader. へのトランザクションの再ブロードキャストに失敗したときに、ネットワークが輻輳しているときに最もよく発生します。エンドユーザーにとっては、トランザクションが完全に消えてしまったかのように見えるかもしれません。 RPC ノードには一般的な再ブロードキャスト アルゴリズムが装備されていますが、アプリケーション開発者は独自のカスタム再ブロードキャスト ロジックを開発することもできます。
概要
Fact Sheet
- RPCノードは、一般的なアルゴリズムを使用してトランザクションを再ブロードキャストしようとします
- アプリケーション開発者は、独自のカスタム再ブロードキャスト ロジックを実装できます
sendTransaction
JSON-RPC メソッドのmaxRetries
パラメータを利用する必要があります- JSON-RPC メソッドのパラメーターを利用する必要があります
- トランザクションが送信される前にプリフライト チェックを有効にしてエラーを発生させる必要があります
- トランザクションに再署名する前に、最初のトランザクションのブロックハッシュの有効期限が切れていることを確認することが非常に重要です
The Journey of a Transaction
クライアントがトランザクションを送信する方法
Solana には mempool という概念はありません。すべてのトランザクションは、プログラムによって開始されたかエンドユーザーによって開始されたかに関係なく、リーダーに効率的にルーティングされるため、ブロックに処理できます。ランザクションをリーダーに送信するには、主に 2 つの方法があります。:
- RPC server経由のプロキシによる sendTransaction JSON-RPC method
- TPU Client経由でリーダーに直接送信
エンドユーザーの大部分は、RPC サーバー経由でトランザクションを送信します。クライアントがトランザクションを送信すると、受信側の RPC ノードが現在と次のリーダーの両方にトランザクションをブロードキャストしようとします。 トランザクションがリーダーによって処理されるまで、クライアントと中継 RPC ノードが認識しているもの以外にトランザクションの記録はありません。 再ブロードキャストとリーダー転送はクライアント ソフトウェアによって完全に処理されます。
RPC ノードがトランザクションをブロードキャストする方法
RPC ノードがsendTransaction
を介してトランザクションを受信した後、関連するリーダーに転送する前に、トランザクションを UDP パケットに変換します。UDP を使用すると、バリデーターは相互に迅速に通信できますが、トランザクションの到達に関する保証はありません。
Solana のリーダー スケジュールは、すべてのepoch (~2 days)前に知られているため、RPC ノードは、そのトランザクションを現在および次のリーダーに直接ブロードキャストします。これは、トランザクションをネットワーク全体にランダムかつ広範に伝播する Ethereum などの他のゴシップ プロトコルとは対照的です。デフォルトでは、トランザクションが終了するか、トランザクションのブロックハッシュが期限切れになるまで(この記事の執筆時点で 150 ブロックまたは約 1 分 19 秒)RPC ノードは 2 秒ごとにトランザクションをリーダーに転送しようとします。 未処理の再ブロードキャスト キューのサイズが 10,000 トランザクションを超える場合、新しく送信されたトランザクションはドロップされます。 コマンドライン引数 を用いてRPCオペレータはこの再試行ロジックのデフォルトの動作を調整し、変更が可能です。
RPC ノードがトランザクションをブロードキャストした時、リーダーのトランザクション処理ユニット (TPU)にトランザクションの転送を施行します。TPU は、5 つの異なるフェーズでトランザクションを処理します:
Image Courtesy of Jito Labs
これら 5 つのフェーズのうち、Fetch Stageはトランザクションの受信を担当します。Fetch ステージ内で、バリデーターは受信トランザクションを 3 つのポートに従って分類します:
- tpu トークン転送、NFT ミント、プログラム命令などの通常のトランザクションを処理します
- tpu_vote 投票トランザクションのみ対応します
- tpu_forwards 現在のリーダーがすべてのトランザクションを処理できない場合、未処理のパケットを次のリーダーに転送します
TPU の詳細については、Jito Labs によるこの優れた記事を参照してください。
How Transactions Get Dropped
トランザクションの行程全体で、トランザクションが意図せずにネットワークからドロップされるシナリオがいくつかあります。
Before a transaction is processed
ネットワークがトランザクションをドロップする場合、トランザクションがリーダーによって処理される前にドロップする可能性が高くなります。 これが発生する最も単純な理由は、UDPの喪失です。ネットワークの負荷が高いときは、バリデーターが処理に必要な膨大な数のトランザクションに圧倒される可能性もあります。 バリデーターはtpu_forwards
を介して余剰トランザクションを転送するように装備されていますが、転送できるデータ量には制限があります。 さらに、各転送はバリデーター間の 1 つのホップに制限されます。つまり、tpu_forwards
ポートで受信したトランザクションは、他のバリデーターに転送されません。
トランザクションが処理される前にドロップされる可能性がある理由として、あまり知られていない 2 つの理由もあります。 トランザクションが処理される前にドロップされる可能性がある理由として、あまり知られていない 2 つの理由もあります。最初のシナリオには、RPC プール経由で送信されるトランザクションが含まれます。これにより、プール内のノードが連携する必要がある場合に問題が発生する可能性があります。この例では、トランザクションのrecentBlockhashがプールの高度な部分 (バックエンド A) からクエリされます。 トランザクションがプールの遅延部分 (バックエンド B) に送信されると、ノードは高度なブロックハッシュを認識せず、トランザクションを破棄します。これは、開発者が sendTransaction
preflight checksを有効にしている場合、トランザクションの送信時に検出できます。
一時的なネットワーク フォークにより、トランザクションがドロップされる可能性もあります。 バリデータがバンキング ステージ内でブロックを再生するのが遅い場合、マイノリティ フォークが作成される可能性があります。 クライアントがトランザクションを構築するとき、そのトランザクションが少数派フォークにのみ存在する recentBlockhash
を参照する可能性があります。トランザクションが送信された後、クラスターは、トランザクションが処理される前に少数フォークから切り替えることができます。このシナリオでは、ブロックハッシュが見つからないため、トランザクションが破棄されます。
After a transaction is processed and before it is finalized
トランザクションがマイノリティ フォークからの recentBlockhash
を参照する場合でも、トランザクションが処理される可能性があります。 ただし、この場合、マイノリティ フォークのリーダーによって処理されます。このリーダーが処理されたトランザクションをネットワークの残りの部分と共有しようとすると、マイノリティフォークを認識しない大多数のバリデーターとの合意に達することができません。この時点で、トランザクションはファイナライズされる前にドロップされます。
Handling Dropped Transactions
RPC ノードはトランザクションの再ブロードキャストを試みますが、使用するアルゴリズムは一般的であり、特定のアプリケーションのニーズには適さないことがよくあります。ネットワークの輻輳に備えて、アプリケーション開発者は独自の再ブロードキャスト ロジックをカスタマイズする必要があります。
An In-Depth Look at sendTransaction
トランザクションの送信に関しては、 sendTransaction
RPC メソッドが開発者が利用できる主要なツールです。sendTransaction
は、クライアントから RPC ノードへのトランザクションの中継のみを担当します。ノードがトランザクションを受信すると、sendTransaction
は、トランザクションの追跡に使用できるトランザクション ID を返します。正常な応答は、トランザクションがクラスターによって処理またはファイナライズされるかどうかを示すものではありません。
TIP
Request Parameters
transaction
:string
- エンコードされた文字列としての完全に署名されたトランザクション- (optional)
configuration object
:object
skipPreflight
:boolean
- true の場合、プリフライト トランザクション チェックをスキップします (default: false)- (optional)
preflightCommitment
:string
- Commitment バンク スロットに対するプリフライト シミュレーションに使用する新しいウィンドウ レベルでのコミットメントオープン (default: "finalized")。 - (optional)
encoding
:string
- トランザクション データに使用されるエンコーディング。 "base58" (slow)または"base64"(default: "base58"). - (optional)
maxRetries
:usize
- RPC ノードがリーダーへのトランザクションの送信を再試行する最大回数。このパラメーターが指定されていない場合、RPC ノードは、トランザクションが終了するか、ブロックハッシュの有効期限が切れるまで、トランザクションを再試行します。
Response
transaction id
:string
- base-58でエンコードされた文字列として、トランザクションに埋め込まれた最初のトランザクション署名。このトランザクション ID を getSignatureStatuses で使用して、ステータスの更新をポーリングできます。
Customizing Rebroadcast Logic
独自の再ブロードキャスト ロジックを開発するには、開発者はsendTransaction
のmaxRetries
パラメータを利用する必要があります。指定された場合、maxRetries
は RPC ノードのデフォルトの再試行ロジックをオーバーライドし、開発者が妥当な範囲内で再試行プロセスを手動で制御できるようにします。
トランザクションを手動で再試行する一般的なパターンには、 getLatestBlockhashから取得したlastValidBlockHeight
を一時的に保存することが含まれます。一旦隠蔽されると、アプリケーションはクラスタのpoll the cluster’s blockheightをポーリングし、適切な間隔で手動でトランザクションを再試行できます。 ネットワークが混雑している場合は、 maxRetries
を0に設定し、カスタム アルゴリズムを介して手動で再ブロードキャストすることをお勧めします。 一部のアプリケーションでは、exponential backoff アルゴリズムで指数バックオフを使用する場合がありますが、Mango などの他のものは、一定の間隔で継続的に トランザクションを再送信することを選択します。
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();
}
})();
while (blockheight < lastValidBlockHeight) {
connection.sendRawTransaction(rawTransaction, {
skipPreflight: true,
});
await sleep(500);
blockheight = await connection.getBlockHeight();
}
getLatestBlockhash
を介してポーリングする場合、 アプリケーションは意図したcommitmentレベルで開くように指定する必要があります。 コミットメントを confirmed
(voted on) または finalized
(~30 blocks after confirmed
)に設定することで、アプリケーションは、マイノリティ フォークからのブロックハッシュのポーリングを回避できます。
アプリケーションがロード バランサーの背後にある RPC ノードにアクセスできる場合、そのワークロードを特定のノードに分割することも選択できます。getProgramAccounts などのデータ集約型のリクエストを処理する RPC ノードは、処理が遅れる傾向があり、トランザクションの転送にも適していない可能性があります。時間に敏感なトランザクションを処理するアプリケーションの場合、sendTransaction
のみを処理する専用ノードを用意するのが賢明な場合があります。
The Cost of Skipping Preflight
デフォルトでは、sendTransaction
は、トランザクションを送信する前に 3 つのプリフライト チェックを実行します。具体的には次のことを行います:
- すべての署名が有効であることを確認する
- 参照されたブロックハッシュが最後の 150 ブロック内にあることを確認します
preflightCommitment
で指定された銀行スロットに対してトランザクションをシミュレートします。
これら 3 つのプリフライト チェックのいずれかが失敗した場合、 sendTransaction
はトランザクションを送信する前にエラーを発生させます。多くの場合、プリフライト チェックは、トランザクションを失うことと、クライアントがエラーを適切に処理できるようにすることの違いになる可能性があります。これらの一般的なエラーが確実に説明されるようにするために、開発者はskipPreflight
をfalse
に設定しておくことをお勧めします。
When to Re-Sign Transactions
再ブロードキャストのあらゆる試みにもかかわらず、クライアントがトランザクションに再署名する必要がある場合があります。ランザクションに再署名する前に、最初のトランザクションのブロックハッシュの有効期限が切れていることを確認することが非常に重要です。最初のブロックハッシュがまだ有効な場合、両方のトランザクションがネットワークによって受け入れられる可能性があります。エンドユーザーには、同じトランザクションを意図せずに2回送信したように見えます。
Solana,では、削除されたトランザクションは、参照するブロックハッシュが getLatestBlockhash
から受け取ったlastValidBlockHeight
よりも古い場合、安全に破棄できます。 開発者は、getEpochInfo
をクエリし、応答のblockHeight
と比較して、この lastValidBlockHeight
を追跡する必要があります。ブロックハッシュが無効になると、クライアントは新しくクエリされたブロックハッシュで再署名できます。
Acknowledgements
Trent Nelson、 Jacob Creech、White Tiger、 Le Yafo、 Buffalu、 Jito Labsのレビューとフィードバックに感謝します。