
優先手数料:Solanaのトランザクション手数料の仕組みを理解する
この記事で解説すること
Solanaは高速です。しかし、利用可能な最速のブロックチェーンであっても、ユーザーは重要なトランザクションの処理を最適化したいと考えます。優先手数料を使うと、ユーザーのトランザクションを実行順序キューの先頭に配置できます。これは、ユーザーがトランザクションに追加できる任意の追加手数料です。
この記事では、Solanaにおけるトランザクション処理の詳細を簡潔に解説します。トランザクションとそのライフサイクル、トランザクション手数料の仕組みを取り上げます。その後、優先手数料、プログラムによる実装方法、ベストプラクティスを解説します。
トランザクションとそのライフサイクル
トランザクションは、Solanaプログラムを呼び出して状態を変更するために使用されます。トランザクションは命令(単一のプログラム呼び出しに対する指示)をまとめたもので、実行する処理、その対象となるアカウント、必要な権限の有無をバリデータに伝えます。
Solanaにおける一般的なトランザクションのライフサイクルは次のとおりです。
- ユーザーは、実行したいアクションについて明確な目的を持っています。たとえば、AliceがBobに10 SOLを送信するとします
- ユーザーは目的のアクションに対応するトランザクションを生成します。たとえば、Aliceは自分のアカウントからBobのアカウントへ10 SOLを転送する命令を含むトランザクションを作成します。また、直近のブロックハッシュを含め、自分の秘密鍵でトランザクションに署名します
- ユーザーはトランザクションをネットワークへ送信します。その後、トランザクションが正常に追加されたかどうかの情報を受け取ります。たとえば、Aliceはconfirmedのコミットメントステータスでトランザクションを送信します。トランザクションが確認されると、トランザクション署名を受け取ります。Aliceはこのトランザクション署名をOrbなどのブロックエクスプローラーで使用し、Bobに10 SOLを正常に送信できたことを確認できます。Aliceのアカウントから10 SOLが引き落とされ、Bobのアカウントには10 SOLが入金されます
ユーザーが署名済みトランザクションをネットワークへ送信するときは、HeliusなどのRPCプロバイダーを利用します。HeliusのRPCはトランザクションを受信し、現在のリーダースケジュールを確認します。Solanaでは、特定の時点で台帳にエントリを追加する役割を担うのは、指定されたバリデータだけです。リーダーは現在のスロットのブロックを生成する役割を担い、連続する4つのスロットが割り当てられます。署名済みトランザクションは、現在のリーダーと次の2つのリーダーへ送信されます。
現在のリーダーは署名済みトランザクションを検証し、その他の前処理を行ってから、トランザクションの実行をスケジュールします。Ethereumなどの他のL1とは異なり、Solanaにはグローバルなトランザクションキューがありません。大半のバリデータは、Solana Labsが提供するスケジューラー実装を使用しています。一方、Jitoバリデータクライアントを実行するバリデータは、疑似メンプール(MempoolStream)を使用してトランザクションの順序を決定します。デフォルトのスケジューラーはマルチスレッドで、各スレッドが実行待ちのトランザクションキューを保持します。トランザクションは、先入れ先出し(FIFO)と優先手数料を組み合わせてブロック内に並べられます。トランザクションは実行スレッドへある程度ランダムに割り当てられるため、この順序は本質的に非決定的である点が重要です。
トランザクションが実行されると、Turbineを介して伝播され、それに応じた手数料が支払われます。
Solanaにおけるトランザクション手数料の仕組み
トランザクション手数料は、Solanaでトランザクションを処理するために支払われる少額の手数料です。現在のリーダーは、ネットワーク経由で送信されたトランザクションを処理し、台帳エントリを生成します。トランザクションが状態の一部として確認されると、Solanaの経済設計を支えるための手数料が支払われます。トランザクション手数料は、基本的に次のような形でSolanaに貢献します。
- バリデータに報酬を提供します
- トランザクションにリアルタイムのコストを課し、ネットワーク領域の消費を抑えます
- プロトコルが徴収する最低手数料を通じて、ネットワークに長期的な経済的安定性をもたらします
Solanaは短期的なネットワークセキュリティを確保するため、プロトコルベースのインフレ報酬に依存しています。これを実現するため、ネットワークにはバリデータへ報酬を与えるためのグローバルなインフレ率が設定されています。長期的には、Solanaはセキュリティを維持するためにトランザクション手数料を利用します。各トランザクション手数料の一定割合(当初は50%に設定)がバーンされ、残りは現在のリーダーへ送られます。Solanaが手数料をバーンするのは、SOLの価値を高めると同時に、悪意のあるバリデータによるトランザクションの検閲を抑止するためです。
トランザクション手数料は、署名ごとに静的に設定された基本手数料と、トランザクション中に使用されるコンピュートユニット(CU)単位の計算リソースに基づいて算出されます。この基本手数料は、署名あたりの目標lamportsの50%から1000%の範囲になります。署名あたりのデフォルト目標値は、現在10,000に設定されています。各トランザクションには、Compute Budgetと呼ばれるCUの最大予算が割り当てられます。この予算を超えると、ランタイムはトランザクションを停止してエラーを返します。トランザクションあたりの最大予算は140万CUで、ブロックスペースの上限は4,800万CUです。
優先手数料とは
こうした制約があるため、計算負荷の高いトランザクションがブロックスペースを埋め、他のトランザクションを遅延させる可能性があります。Solanaは、リーダーのキュー内で他のトランザクションよりも自身のトランザクションを優先できる任意の手数料を導入しました。これが優先手数料です。この手数料を支払うことでトランザクションが実質的に優先され、実行時間が短縮されます。これは、時間的制約のあるトランザクションや高額なトランザクションに有効です。トランザクションの手数料優先度は、要求するコンピュートユニット数によって決まります。要求するコンピュートユニットが多いほど、トランザクションキュー内で優先度を維持するために支払う手数料も高くなります。コンピュートユニット数に応じて手数料を増やすことで、計算負荷の高いトランザクションによるスパムを防ぎます。
優先手数料は、トランザクションのコンピュート予算と、micro-lamports単位で測定されるコンピュートユニット価格を掛け合わせたものです。priorityFees = computeBudget * computeUnitPrice
コンピュート予算
computeBudgetは、トランザクションが消費できるコンピュートユニットの最大数、トランザクションが実行する各種処理に伴うコスト、トランザクションが従うべき実行上の制約を定めます。次の処理ではコンピュートコストが発生します。
- SBF命令の実行
- プログラム間でのデータ受け渡し
- システムコールの呼び出し(ロギング、プログラムアドレスの作成、CPIなど)
クロスプログラム呼び出し(CPI)では、呼び出されたプログラムは、呼び出し元(親)プログラムのコンピュート予算内で動作します。呼び出されたプログラムが残りのコンピュート予算をすべて使い切るか、設定された上限を超えると、プログラム呼び出しチェーン全体が失敗します。これには、処理を開始した最初のトランザクションの実行も含まれます。
現在のコンピュート予算はこちらで確認できます。
優先手数料をプログラムで実装する方法
トランザクションの優先手数料は、SetComputeUnitPrice命令https://github.com/solana-labs/solana/blob/2971e84ec87815adb1e4def95cbcd8d0d96845f5/sdk/src/compute_budget.rs#L59と、任意のSetComputeUnitLimit命令を設定することで指定します。SetComputeUnitPrice命令を指定しない場合、追加手数料がないため、トランザクションはデフォルトで最低優先度になります。SetComputeUnitLimit命令を指定しない場合、上限はトランザクション内の命令数とデフォルトのコンピュートユニット上限の積として計算されます。ランタイムはコンピュートユニット価格とコンピュートユニット上限を使用して優先手数料を算出し、その値に基づいて対象のトランザクションを優先します。
優先手数料をプログラムで追加するには、対象のトランザクションにこれらの命令を追加する必要があります。Javascriptでは次のようになります。
import {
Keypair,
Connection,
PublicKey,
Transaction,
SystemProgram,
LAMPORTS_PER_SOL,
sendAndConfirmTransaction,
ComputeBudgetProgram,
} from "@solana/web3.js";
async function main() {
// Initialize an RPC client
const clusterUrl = "http://127.0.0.1:8899";
const connection = new Connection(clusterUrl, "confirmed");
// Initialize new sender and receiver keypairs
const fromKeypair = Keypair.generate();
const toPubkey = new PublicKey(Keypair.generate().publicKey);
// Airdrop SOL to the from_keypair
const airdropAmount = 100 * LAMPORTS_PER_SOL;
try {
const signature = await connection.requestAirdrop(
fromKeypair.publicKey,
airdropAmount
);
console.log("Airdrop requested. Signature:", signature);
await connection.confirmTransaction({
signature,
confirmation: "confirmed",
});
} catch (e) {
console.error("Failed to request airdrop:", e);
return;
}
// Check if airdrop was successful
const balance = await connection.getBalance(fromKeypair.publicKey);
if (balance < airdropAmount) {
console.error(
"Airdrop was not successful. The current balance is insufficient"
);
return;
}
// Airdrop SOL to the toPubkey
const airdropAmountTo = 100 * LAMPORTS_PER_SOL; // 1 SOL in lamports
try {
const signature = await connection.requestAirdrop(toPubkey, airdropAmount);
console.log("Airdrop requested. Signature:", signature);
await connection.confirmTransaction({
signature,
confirmation: "confirmed",
});
} catch (e) {
console.error("Failed to request airdrop:", e);
return;
}
// Check if airdrop was successful
const balanceTo = await connection.getBalance(toPubkey);
if (balance < airdropAmount) {
console.error(
"Airdrop was not successful. The current balance is insufficient"
);
return;
}
console.log(`Account balance: ${balance / LAMPORTS_PER_SOL} SOL`);
// Create the priority fee instructions
const computePriceIx = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: 1,
});
const computeLimitIx = ComputeBudgetProgram.setComputeUnitLimit({
units: 200_000,
});
// Create the transfer instruction
const transferIx = SystemProgram.transfer({
fromPubkey: fromKeypair.publicKey,
toPubkey,
lamports: 100_000,
});
// Create the transaction with priority fees
const transaction = new Transaction().add(
computePriceIx,
computeLimitIx,
transferIx
);
// Fetch the recent blockhash and sign the transaction
transaction.recentBlockhash = (
await connection.getLatestBlockhash()
).blockhash;
transaction.sign(fromKeypair);
// Send the transaction
try {
const txid = await sendAndConfirmTransaction(connection, transaction, [
fromKeypair,
]);
console.log("Transaction sent successfully with signature", txid);
} catch (e) {
console.error("Failed to send transaction:", e);
}
}
main();このスニペットでは、次の処理を行います。
- Localhostを使用してテスト環境を設定します
- 2つの新しいウォレット(
fromKeypairとtoPubkey)を作成します - 両方のウォレットへ100 SOLをエアドロップします
- エアドロップが成功したことを確認します
- 優先手数料の命令(
computePriceIxとcomputeLimitIx)を作成します fromKeypairからtoPubkeyへ100, 000 lamportsを送信する転送命令を作成します- 新しいトランザクションを作成し、すべての命令を追加します
- 最新のブロックハッシュをトランザクションに付与し、署名します
- トランザクションを送信し、正常に送信されたことを確認します
これだけです。優先手数料は、より高速に実行してもらうための支払いとしてトランザクションに追加する命令にすぎません。このスニペットの大部分は、開発環境と、SOLを相互に転送する2つのウォレットを設定しています。ここで注目すべきなのは、優先手数料を追加する命令を作成し、それらをトランザクションへ追加する部分です。
// Other code
const computePriceIx = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: 1,
});
const computeLimitIx = ComputeBudgetProgram.setComputeUnitLimit({
units: 200_000,
});
// Other code
const transaction = new Transaction().add(computePriceIx, computeLimitIx, transferIx);
// Rest of the codeベストプラクティス
これらの命令は順番に実行されるため、記述順が重要です。たとえば、コンピュート上限を拡張する前にトランザクションがデフォルトのコンピュート上限(200k CU)を超えると、そのトランザクションは失敗します。次のようなトランザクションがあるとします。
- 命令1は100kを使用します
- 命令2は150kを使用します
- 命令3はコンピュート上限を拡張します
このトランザクションは、命令2と3を入れ替えない限り失敗します。そのため、トランザクションへ他の命令を追加する前に、コンピュート上限の命令を追加することを推奨します。トランザクションに優先手数料を追加するためにSetComputeLimit命令を使用する必要はありません。これは完全に任意です。SetComputePrice命令を配置する位置は影響しません。
直近に支払われた優先手数料の一覧を取得するには、getRecentPrioritizationFees RPCメソッドの使用を推奨します。このデータを使用すると、クラスターで確実に処理され、支払額を最小限に抑えられる適切な優先手数料を推定できます。また、Heliusは新しいPriority Fee APIも提供しています。次のセクションで解説します。
トランザクションでは、手数料を最小限に抑えるため、実行に必要な最小限のコンピュートユニット数を要求する必要もあります。要求したコンピュートユニット数がトランザクションで実際に使用された総ユニット数を超えても、コストは調整されない点に注意してください。
Phantomなど一部のウォレットプロバイダーは、dAppがトランザクションの優先手数料を設定できることを認識しています。しかし、エンドユーザーに不要な複雑さをもたらすことが多いとして、その使用を推奨していません。代わりに、dApp開発者には、ユーザーに代わってPhantomが優先手数料を適用できるようにすることを推奨しています。たとえば、SolfareはSolanaの負荷状況を自動的に検出し、他のトランザクションよりも優先されるように手数料をわずかに引き上げて、この問題に対処しています。
Helius Priority Fee API
getRecentPrioritizationFees RPCメソッドを使用して優先手数料を計算する方法は、スロット単位で手数料を算出するうえで直感的です。しかし、ネットワーク状況が常に変化することや、getRecentPrioritizationFees**'**のレスポンスの性質を考えると、難しい場合があります。このレスポンスは過去150ブロックの値の一覧を返すため、手数料として設定する最低値を判断する場合にしか役立ちません。
Helius Priority Fee APIには、グローバルおよびローカルの手数料市場を考慮し、レスポンスを単一の値に簡素化する新しいメソッドgetPriorityFeeEstimateが導入されています。このメソッドは、事前定義されたパーセンタイルのセットを使用して推定値を算出します。これらのパーセンタイル、つまりレベルは、NONE(0パーセンタイル)からUNSAFE_MAX(100パーセンタイル。ユーザーが誤って資金を使い切るのを防ぐためunsafeと表示)までです。ユーザーは、すべての優先度レベルを受け取るよう指定したり、lookbackSlotsを介して計算に使用する範囲を調整したりすることもできます。そのため、getRecentPrioritizationFeesと同様に、ユーザーは推定値の計算時に一定数のスロットを遡れます。
たとえば、次のスクリプトを使用すると、Jupiter v6プログラムのすべての優先手数料レベルを計算できます。
const url = `https://mainnet.helius-rpc.com/?api-key=`;
const getRecentPrioritizationFees = async () => {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "getPriorityFeeEstimate",
params: [{
"accountKeys": ["JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"],
"options": {
"includeAllPriorityFeeLevels": true,
}
}]
}),
});
const data = await response.json();
console.log("Fee: ", data);
};
getRecentPrioritizationFees();このエンドポイントは現在も活発に開発されています。詳細については、Heliusのドキュメントをご覧ください。
まとめ
Solanaトランザクションの世界を詳しく見ると、ネットワーク効率と経済的インセンティブのバランスを取る高度なシステムがあることが分かります。トランザクションの仕組みに加え、通常の手数料と優先手数料を理解することで、開発者とユーザーはSolana上でのやり取りを最適化するために、より多くの情報に基づいた判断を下せます。優先手数料をプログラムで実装できることで、高額なトランザクションや時間的制約のあるトランザクションに新たな可能性が広がります。これにより、Solana上の操作をさらに柔軟かつ効率的に行えます。
ここまでお読みいただき、ありがとうございます!Solanaの最新情報を見逃さないよう、下にメールアドレスを入力してください。さらに深く学ぶ準備はできましたか?今すぐDiscordに参加し、最高のパフォーマンスを誇るブロックチェーン上で未来を築き始めましょう。
その他のリソース/参考資料
関連記事
Heliusを購読
Solana開発の最新情報や新しい記事の公開通知を受け取れます


