概要
Solanaには3つのトランザクションフォーマットがあります:legacy、v0、v1。v0は1バイトインデックスでアカウントを参照するアドレスルックアップテーブル(ALT)を追加します。v1はサイズ制限を4,096バイトに引き上げ、リソース制限をメッセージ自体に組み込み、ALTを廃止します。
Solanaは3つのトランザクションフォーマットをサポートしています:legacy、v0、v1。それぞれについて、ワイヤー上のバイト配置、アカウントの参照方法、リソース制限の出所という3つの観点から説明します。
フォーマット比較
| 制限 | legacy | v0 | v1 |
|---|---|---|---|
| 最大トランザクションサイズ | 1,232バイト | 1,232バイト | 4,096バイト |
| アカウントアドレス | ~32個、サイズ制限あり | 64個、ルックアップテーブル経由 | 64個、インライン(SIMD-0596適用時は96個) |
| アドレスルックアップテーブル | 非対応 | 対応 | 非対応 |
| リソース制限 | ComputeBudget instructions | ComputeBudget instructions | メッセージ設定 |
Legacyフォーマット
オリジナルのフォーマットであり、ほとんどのツールでいまだにデフォルトとして使用されています。バージョンプレフィックスは一切なく、トランザクションの最初のバイトはシグネチャ配列のcompact-u16カウント、メッセージの最初のバイトはnum_required_signaturesであり、その最上位ビットは常に未設定です。
Legacyワイヤーレイアウト
| フィールド | サイズ | 説明 |
|---|---|---|
num_signatures | compact-u16 | シグネチャの数 |
signatures | num_signatures x 64バイト | Ed25519シグネチャ |
header | 3バイト | MessageHeader — 最初のバイトはバージョンビット未設定 |
num_account_keys | compact-u16 | アカウントキーの数 |
account_keys | num_account_keys x 32バイト | 公開鍵、すべてインライン |
recent_blockhash | 32バイト | 有効期限指定子 |
num_instructions | compact-u16 | instructionsの数 |
instructions | 可変長 | 各instructionを連続してシリアライズ |
可変長配列はすべてcompact-u16長プレフィックスが付きます:値0〜127は1バイト、より大きい値は2〜3バイトです。instruction単位のレイアウトとサイズ計算の例については、トランザクションバイナリフォーマットを参照してください。
Legacyのアカウント
すべてのアカウントはaccount_keys内に32バイトの完全な公開鍵として書き込まれ、instructionsは1バイトのインデックスでその配列を参照します。トランザクションに明示的に記載されていないアカウントを参照する方法はなく、これがlegacyトランザクションを1,232バイトを使い切る前におよそ32アカウントに制限する要因です。
Legacyのリソース制限
コンピュートユニット制限、ロード済みアカウントデータサイズ制限、ヒープサイズ、優先手数料はすべて、トランザクションにComputeBudgetプログラムのinstructionsを含めることでリクエストされます。それぞれがinstructionスロットを1つと150コンピュートユニットを消費します。省略しても安全です:ランタイムはデフォルト値(instruction当たり200,000 CU、上限1.4M、データサイズ制限64 MiB、ヒープ32 KiB、優先手数料ゼロ)にフォールバックします。
V0フォーマット
v0はlegacyメッセージに2つの要素を加えたものです:0x80バージョンプレフィックスバイトと、instructionsの後に追加されるaddress_table_lookups配列です。それ以前の部分はlegacyとバイト単位で同一です。
V0ワイヤーレイアウト
| フィールド | サイズ | 説明 |
|---|---|---|
num_signatures | compact-u16 | シグネチャの数 |
signatures | num_signatures x 64バイト | Ed25519シグネチャ |
0x80 | 1バイト | バージョンプレフィックスバイト — メッセージの最初のバイト |
header | 3バイト | MessageHeader(レガシーと同じ) |
num_account_keys | compact-u16 | 静的アカウントキーの数 |
static_account_keys | num_account_keys x 32バイト | トランザクション内に文字通り表示されるキー |
recent_blockhash | 32バイト | 有効期限指定子 |
num_instructions | compact-u16 | instructionsの数 |
instructions | 可変長 | レガシーと同じ形式 |
address_table_lookups | compact-u16 + 可変 | ALT参照(下記参照) |
各アドレステーブルルックアップエントリには以下が含まれます:
| フィールド | サイズ | 説明 |
|---|---|---|
account_key | 32バイト | ALTアカウントの公開鍵 |
writable_indexes | compact-u16 + N x 1バイト | 書き込み可能アカウント用のALTへのインデックス |
readonly_indexes | compact-u16 + N x 1バイト | 読み取り専用アカウント用のALTへのインデックス |
アドレスルックアップテーブル
ALTは最大256個の公開鍵を保存するオンチェーンアカウントです。ALTを参照することで、トランザクションは32バイトの公開鍵の代わりに1バイトのインデックスを使用して追加のアカウントを含めることができ、アカウントごとのオーバーヘッドを大幅に削減します。
実行開始前のランタイムにおいて、validatorはすべてのALT参照を完全な公開鍵に解決します。解決されたアドレスは静的アカウントキーに追加され、完全なアカウントキーリストを形成します。ALTで解決されたアカウントは、静的アカウントと同じ順序に従います。書き込み可能なルックアップが読み取り専用ルックアップの前に配置されます。
アドレスルックアップテーブルは、オンワイヤトランザクションでアカウントがどのように参照されるかにのみ影響します。実行時には、ランタイムがすべてのインデックスを完全なアカウントアドレスに解決します。ALTで解決されたアカウントは書き込み可能または読み取り専用(非署名者)のみ可能で、署名者にはなれません。
アドレスルックアップテーブルの作成と使用
この例では、ルックアップテーブルを作成してアドレスを追加し、そのテーブルを使ってv0トランザクションをコンパイルして送信します。
ルックアップテーブルからロードできるのは、署名者ではないアカウントのみです。手数料支払者と必要なすべての署名者は、メッセージの静的アカウントリストに残ります。また、アドレスは追加されたslotと同じslotではルックアップできないため、この例ではv0メッセージをコンパイルする前に、次にconfirmedとなるslotまで待機します。
v0のリソース制限
legacyから変更なし:ComputeBudget instructions、省略時のデフォルト値も同様。
V1フォーマット
v1はサイズ制限を4,096バイトに引き上げ、トランザクション設定を中心にメッセージを再構築します:リソース制限はComputeBudget instructionsから外れ、メッセージ自体の固定位置フィールドに移動します。これにより、ネットワークはinstructionリストをスキャン・デシリアライズせず、単一の固定オフセット読み取りでトランザクションを優先手数料でランク付けできます。
V1ワイヤーレイアウト
| フィールド | サイズ | 説明 |
|---|---|---|
0x81 | 1バイト | バージョンプレフィックスバイト — トランザクションの最初のバイト |
header | 3バイト | MessageHeader(レガシーと同じ) |
config_mask | 4バイト | どの設定値が存在するかを示すu32 LEビットマスク |
recent_blockhash | 32バイト | 有効期限指定子 |
num_instructions | 1バイト | 固定幅カウント、最大64 |
num_addresses | 1バイト | 固定幅カウント、最大64(SIMD-0596適用時は96) |
addresses | N x 32バイト | アカウントアドレス、すべてインライン — ルックアップテーブル参照なし |
config_values | 0〜20バイト | 設定されたマスクビットごとに1つの値、ビット順(下記参照) |
instruction_headers | N x 4バイト | instruction単位:program_id_index(u8)、num_accounts(u8)、data_len(u16 LE) |
instruction_payloads | 可変長 | instruction単位:アカウントインデックス、続いてinstruction data |
signatures | N x 64バイト | 末尾に配置、長さプレフィックスなし — カウントはヘッダーから取得 |
デコーダーを実装する際に注意すべきlegacyおよびv0との構造上の相違点が2つあります。カウントはcompact-u16ではなく固定幅のu8フィールドであること、そしてinstructionsは各instructionを連続して配置するのではなく、固定サイズのヘッダーをすべて先に並べ、その後に可変長ペイロードをすべて並べるという2つのランクに分割されることです。
v1のアカウント:アドレスルックアップテーブルなし
v1では、ALTのサポートを意図的に削除しています。生のアドレス64個は2,048バイト、SIMD-0596後に許可される96アドレスは3,072バイトで、いずれも4,096バイトの制限に十分収まるため、legacyと同様にすべてのアドレスがインラインになります。アプリケーションがルックアップテーブルに依存している場合、v1への移行にはそれらのアドレスをインライン化する作業が必要になります。
v1のリソース制限:トランザクション設定
設定はu32ビットマスクに続いて、そのビットが設定された各フィールドの固定幅値で構成されます:
| ビット | フィールド | 幅 | 備考 |
|---|---|---|---|
| 0〜1 | 優先手数料 | u64 | 合計lamport数 — 両ビットを同時に設定 |
| 2 | コンピュートユニット制限 | u32 | |
| 3 | ロード済みアカウントデータサイズ制限 | u32 | |
| 4 | 要求ヒープサイズ | u32 |
未知のビットは拒否されます。メッセージは署名されているため、未認識の設定フィールドを暗黙的に破棄することはできません。
優先手数料は単価ではなく合計lamport数
legacyおよびv0では、優先手数料はSetComputeUnitPriceを通じてコンピュートユニット当たりのマイクロlamport数として設定され、コンピュートユニット制限と乗算されます。v1ではlamport単位の絶対合計値となり、乗算も丸めも不要です。CU単位の演算をそのまま持ち込まないでください。合計手数料の計算式はその他の点では変わりません:(signatures × lamports_per_signature) + priority_fee。
v1でのComputeBudget instructionsはno-op
v1トランザクションはComputeBudget instructionsを拒否しません — 設定においては無視されます。これらは引き続き成功したno-opとして実行され、150コンピュートユニットと64あるinstructionスロットのうちの1つを消費しますが、バジェットには何の影響も与えません。v1トランザクションを構築する際はこれらを取り除き、v1トランザクションを読み取る際はスキャンを止めてください:値はメッセージ設定に格納されています。
設定フィールドは明示的に指定する必要があります
送信者にとって最も重要な動作上の変更点:legacyおよびv0とは異なり、v1トランザクションはコンピュートユニット制限とロード済みアカウントデータサイズ制限を明示的に設定しなければならず、設定しない場合トランザクションは失敗します。
| 未設定フィールド | legacy / v0 | v1 | 省略時の症状 |
|---|---|---|---|
| コンピュートユニット制限 | instruction当たり200k、最大1.4M | 0 CU | 即時失敗、予算不足 |
| ロード済みアカウントデータサイズ | 64 MiB | 0バイト | 最初にロードされたアカウントでMaxLoadedAccountsDataSizeExceeded |
| 優先手数料 | 0 | 0 | — |
| ヒープサイズ | 32 KiB | 32 KiB | — |
推奨されるアプローチは、両方の制限を最大値にして一度シミュレーションを実行し、返されたunitsConsumedとloadedAccountsDataSizeを設定に書き戻すことです。データサイズは余裕を持たせるために次の32 KiBページに切り上げてください(ブロックコストモデルは32 KiBページ単位で課金されるため、次のページ境界未満の余裕はコストがかかりません)。
v1トランザクションの構築
v1メッセージは、legacyやv0のメッセージと同じパイプラインで組み立てます。違いはsetTransactionMessageConfigです。リソース制限はメッセージのプロパティであるため、instructionリストにはプログラムのinstructionsのみが含まれます。
import {appendTransactionMessageInstruction,assertIsTransactionWithBlockhashLifetime,createClient,createTransactionMessage,generateKeyPairSigner,getSignatureFromTransaction,lamports,pipe,sendAndConfirmTransactionFactory,setTransactionMessageConfig,setTransactionMessageFeePayerSigner,setTransactionMessageLifetimeUsingBlockhash,signTransactionMessageWithSigners,} from "@solana/kit";import { rpcAirdrop, solanaRpc } from "@solana/kit-plugin-rpc";import { airdropPayer, payer } from "@solana/kit-plugin-signer";import { getTransferSolInstruction } from "@solana-program/system";const sender = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();const client = await createClient().use(payer(sender)).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900",}),).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n)));const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();// A v1 message is assembled through the same pipeline as a legacy or v0 one.// The difference is the config: a v1 transaction carries its resource limits in// the message itself, so no ComputeBudget instructions are compiled in.const transactionMessage = pipe(createTransactionMessage({ version: 1 }),(m) => setTransactionMessageFeePayerSigner(sender, m),(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),(m) =>appendTransactionMessageInstruction(getTransferSolInstruction({source: sender,destination: recipient.address,amount: lamports(10_000_000n),}),m,),// The compute unit limit and loaded accounts data size limit both default to// zero on v1, so a transaction that leaves them unset always fails. The// priority fee is an absolute total in lamports, not a price per compute// unit.(m) =>setTransactionMessageConfig({computeUnitLimit: 20_000,loadedAccountsDataSizeLimit: 64 * 1024,priorityFeeLamports: 5_000n,},m,),);const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);assertIsTransactionWithBlockhashLifetime(signedTransaction);await sendAndConfirmTransactionFactory({rpc: client.rpc,rpcSubscriptions: client.rpcSubscriptions,})(signedTransaction, { commitment: "confirmed" });const transactionSignature = getSignatureFromTransaction(signedTransaction);console.log("Transaction Signature:", transactionSignature);
Kitは、configフィールドごとのセッター — setTransactionMessagePriorityFeeLamports、setTransactionMessageComputeUnitLimit、setTransactionMessageLoadedAccountsDataSizeLimit、setTransactionMessageHeapSize — も提供しており、1つずつ設定できます。
シミュレーションによる制限値の見積もり
2つの必須の制限値は推測ではなく、実測しましょう。シミュレーションは両方の制限をランタイムの最大値に引き上げて実行されるため、測定対象のリソース不足で失敗することはありません。返される数値は、1回のシミュレーション実行の正確なコストですので、利用する前にマージンを加えてください。
import {appendTransactionMessageInstruction,assertIsTransactionWithBlockhashLifetime,createClient,createTransactionMessage,estimateAndSetResourceLimitsFactory,estimateResourceLimitsFactory,fillTransactionMessageProvisoryResourceLimits,generateKeyPairSigner,getSignatureFromTransaction,lamports,pipe,sendAndConfirmTransactionFactory,setTransactionMessageFeePayerSigner,setTransactionMessageLifetimeUsingBlockhash,setTransactionMessagePriorityFeeLamports,signTransactionMessageWithSigners,} from "@solana/kit";import { rpcAirdrop, solanaRpc } from "@solana/kit-plugin-rpc";import { airdropPayer, payer } from "@solana/kit-plugin-signer";import { getTransferSolInstruction } from "@solana-program/system";const sender = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();const client = await createClient().use(payer(sender)).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900",}),).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n)));const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();// The priority fee is a pricing decision that simulation cannot measure, so it// is set directly. The two resource limits are measured below instead of// guessed. The provisory fill writes placeholder limits so the message// simulates at the same size it will be sent at.const draftMessage = pipe(createTransactionMessage({ version: 1 }),(m) => setTransactionMessageFeePayerSigner(sender, m),(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),(m) =>appendTransactionMessageInstruction(getTransferSolInstruction({source: sender,destination: recipient.address,amount: lamports(10_000_000n),}),m,),(m) => setTransactionMessagePriorityFeeLamports(5_000n, m),fillTransactionMessageProvisoryResourceLimits,);// Simulation runs with both limits raised to the runtime maximum, so it cannot// fail for want of the resources it is measuring.const estimateAndSetResourceLimits = estimateAndSetResourceLimitsFactory(estimateResourceLimitsFactory({ rpc: client.rpc }),);const transactionMessage = await estimateAndSetResourceLimits(draftMessage, {commitment: "confirmed",});console.log("Compute unit limit:", transactionMessage.config?.computeUnitLimit);console.log("Loaded accounts data size limit:",transactionMessage.config?.loadedAccountsDataSizeLimit,);const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);assertIsTransactionWithBlockhashLifetime(signedTransaction);await sendAndConfirmTransactionFactory({rpc: client.rpc,rpcSubscriptions: client.rpcSubscriptions,})(signedTransaction, { commitment: "confirmed" });// The estimate is the exact cost of one simulated run, with nothing to spare.// Add a margin before relying on it in production.console.log("Transaction Signature:",getSignatureFromTransaction(signedTransaction),);
v1への移行準備
v1は、トランザクションの送信だけでなく読み取りにも変更をもたらします。オプトインせずにgetTransactionやgetBlockを呼び出すクライアントは、v1トランザクションで失敗します:
getTransactionおよびgetBlockにmaxSupportedTransactionVersion: 1— 文字列"1"ではなくJSON整数の1— を渡してください。0を渡すとパラメーターを省略した場合と同様にv1トランザクションで失敗します。v0のロールアウト時にコードベースを更新していても、値を変更する必要があります。getTransactionはv1トランザクションに対してエラー-32015で失敗し、getBlockレスポンスに1つでもv1トランザクションがあると、同じエラーでレスポンス全体が失敗します — 部分的な結果は返されません。blockSubscribeはblock: nullを返して進行を停止するため、これを空のブロックとして読み取るコンシューマーは最初のv1 slot以降から静かに遅延します。getSignaturesForAddressはトランザクション本体を検査しないため、v1のシグネチャは通常通り一覧表示されます。- オプトインしたレスポンスには、v1トランザクションの場合、メッセージ内に
transactionConfigオブジェクトが含まれます(legacyとv0では一切含まれません)。ComputeBudget instructionsをスキャンしてプライオリティフィーやコンピュートリミットを導き出すパイプラインは、すべてのv1トランザクションに対してゼロと報告してしまい、そのことに気づけません。 - クライアント側でトランザクションをデコードする場合、および1,232バイトを超えるトランザクションの
sendTransaction/simulateTransactionにはencoding: 'base64'を使用してください — base58エンコーディングは旧来のサイズ上限のままです。 - クライアントライブラリのサポートには最新バージョンが必要です:
@solana/kit8.0以降、Agave 4.2.x世代のRustクレート、またはweb3.js v3。web3.js v1は1.99.0以降でv1を読み取れますが、構築または送信はできません。
v1トランザクションの読み取り
オプトインするには、JSON整数の1を指定します。返されたバイト列をデコードすると、バージョンとconfigが得られます。リソース制限はここに格納されています — instructionリストをスキャンしてComputeBudget instructionsを探しても、何も見つかりません。
import {decompileTransactionMessage,getBase64Encoder,getCompiledTransactionMessageDecoder,getTransactionDecoder,} from "@solana/kit";// Reading a v1 transaction requires opting in. The integer 1 is mandatory:// omitting the parameter, or passing 0, fails on a v1 transaction rather than// returning it in a degraded form.const fetched = await client.rpc.getTransaction(transactionSignature, {commitment: "confirmed",encoding: "base64",maxSupportedTransactionVersion: 1,}).send();if (fetched === null) {throw new Error("the transaction just sent was not found");}// The compiled message stores the config as a bitmask plus a positional list,// so it is decompiled to address the fields by name. Decompiling fetches no// accounts, because v1 does not support address lookup tables.const wireTransaction = getBase64Encoder().encode(fetched.transaction[0]);const transaction = getTransactionDecoder().decode(wireTransaction);const compiledMessage = getCompiledTransactionMessageDecoder().decode(transaction.messageBytes,);const message = decompileTransactionMessage(compiledMessage);console.log("Version:", message.version);if ("config" in message) {// Scanning the instruction list for ComputeBudget instructions finds nothing// here — on v1 the limits live in the config.console.log("Compute unit limit:", message.config?.computeUnitLimit);console.log("Priority fee (lamports):", message.config?.priorityFeeLamports);}
クライアントライブラリのサポート、ストリーミング(Geyser/gRPC)でのバージョン検出、シミュレーションの挙動など、移行ガイドの全容については、Transaction Format v1アップグレードページをご覧ください。TypeScript、Rust、Python、Goの実行可能なサンプルはsolana-foundation/transaction-v1-examplesにあります。
Is this page helpful?