バージョン付きトランザクション

概要

Solanaには3つのトランザクションフォーマットがあります:legacy、v0、v1。v0は1バイトインデックスでアカウントを参照するアドレスルックアップテーブル(ALT)を追加します。v1はサイズ制限を4,096バイトに引き上げ、リソース制限をメッセージ自体に組み込み、ALTを廃止します。

Solanaは3つのトランザクションフォーマットをサポートしています:legacy、v0、v1。それぞれについて、ワイヤー上のバイト配置、アカウントの参照方法、リソース制限の出所という3つの観点から説明します。

v1の有効化状況

v1フォーマットは、mainnet、devnet、testnetで有効です。トランザクションの読み取りには破壊的変更が伴います。オプトインしていないクライアントは、v1トランザクションで失敗します。既存のアプリはv1への移行準備を確認してください。

フォーマット比較

制限legacyv0v1
最大トランザクションサイズ1,232バイト1,232バイト4,096バイト
アカウントアドレス~32個、サイズ制限あり64個、ルックアップテーブル経由64個、インライン(SIMD-0596適用時は96個)
アドレスルックアップテーブル非対応対応非対応
リソース制限ComputeBudget instructionsComputeBudget instructionsメッセージ設定

ジャンプ先:Legacy · v0 · v1

Legacyフォーマット

オリジナルのフォーマットであり、ほとんどのツールでいまだにデフォルトとして使用されています。バージョンプレフィックスは一切なく、トランザクションの最初のバイトはシグネチャ配列のcompact-u16カウント、メッセージの最初のバイトはnum_required_signaturesであり、その最上位ビットは常に未設定です。

Legacyワイヤーレイアウト

フィールドサイズ説明
num_signaturescompact-u16シグネチャの数
signaturesnum_signatures x 64バイトEd25519シグネチャ
header3バイトMessageHeader — 最初のバイトはバージョンビット未設定
num_account_keyscompact-u16アカウントキーの数
account_keysnum_account_keys x 32バイト公開鍵、すべてインライン
recent_blockhash32バイト有効期限指定子
num_instructionscompact-u16instructionsの数
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_signaturescompact-u16シグネチャの数
signaturesnum_signatures x 64バイトEd25519シグネチャ
0x801バイトバージョンプレフィックスバイト — メッセージの最初のバイト
header3バイトMessageHeader(レガシーと同じ)
num_account_keyscompact-u16静的アカウントキーの数
static_account_keysnum_account_keys x 32バイトトランザクション内に文字通り表示されるキー
recent_blockhash32バイト有効期限指定子
num_instructionscompact-u16instructionsの数
instructions可変長レガシーと同じ形式
address_table_lookupscompact-u16 + 可変ALT参照(下記参照)

各アドレステーブルルックアップエントリには以下が含まれます:

フィールドサイズ説明
account_key32バイトALTアカウントの公開鍵
writable_indexescompact-u16 + N x 1バイト書き込み可能アカウント用のALTへのインデックス
readonly_indexescompact-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ワイヤーレイアウト

フィールドサイズ説明
0x811バイトバージョンプレフィックスバイト — トランザクションの最初のバイト
header3バイトMessageHeader(レガシーと同じ)
config_mask4バイトどの設定値が存在するかを示すu32 LEビットマスク
recent_blockhash32バイト有効期限指定子
num_instructions1バイト固定幅カウント、最大64
num_addresses1バイト固定幅カウント、最大64(SIMD-0596適用時は96)
addressesN x 32バイトアカウントアドレス、すべてインライン — ルックアップテーブル参照なし
config_values0〜20バイト設定されたマスクビットごとに1つの値、ビット順(下記参照)
instruction_headersN x 4バイトinstruction単位:program_id_index(u8)、num_accounts(u8)、data_len(u16 LE)
instruction_payloads可変長instruction単位:アカウントインデックス、続いてinstruction data
signaturesN 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 / v0v1省略時の症状
コンピュートユニット制限instruction当たり200k、最大1.4M0 CU即時失敗、予算不足
ロード済みアカウントデータサイズ64 MiB0バイト最初にロードされたアカウントでMaxLoadedAccountsDataSizeExceeded
優先手数料00—
ヒープサイズ32 KiB32 KiB—

推奨されるアプローチは、両方の制限を最大値にして一度シミュレーションを実行し、返されたunitsConsumedとloadedAccountsDataSizeを設定に書き戻すことです。データサイズは余裕を持たせるために次の32 KiBページに切り上げてください(ブロックコストモデルは32 KiBページ単位で課金されるため、次のページ境界未満の余裕はコストがかかりません)。

v1トランザクションの構築

v1メッセージは、legacyやv0のメッセージと同じパイプラインで組み立てます。違いはsetTransactionMessageConfigです。リソース制限はメッセージのプロパティであるため、instructionリストにはプログラムのinstructionsのみが含まれます。

Kit
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);
Console
Click to execute the code.

Kitは、configフィールドごとのセッター — setTransactionMessagePriorityFeeLamports、setTransactionMessageComputeUnitLimit、setTransactionMessageLoadedAccountsDataSizeLimit、setTransactionMessageHeapSize — も提供しており、1つずつ設定できます。

シミュレーションによる制限値の見積もり

2つの必須の制限値は推測ではなく、実測しましょう。シミュレーションは両方の制限をランタイムの最大値に引き上げて実行されるため、測定対象のリソース不足で失敗することはありません。返される数値は、1回のシミュレーション実行の正確なコストですので、利用する前にマージンを加えてください。

Kit
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),
);
Console
Click to execute the code.

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/kit 8.0以降、Agave 4.2.x世代のRustクレート、またはweb3.js v3。web3.js v1は1.99.0以降でv1を読み取れますが、構築または送信はできません。

v1トランザクションの読み取り

オプトインするには、JSON整数の1を指定します。返されたバイト列をデコードすると、バージョンとconfigが得られます。リソース制限はここに格納されています — instructionリストをスキャンしてComputeBudget instructionsを探しても、何も見つかりません。

Kit
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);
}
Console
Click to execute the code.

クライアントライブラリのサポート、ストリーミング(Geyser/gRPC)でのバージョン検出、シミュレーションの挙動など、移行ガイドの全容については、Transaction Format v1アップグレードページをご覧ください。TypeScript、Rust、Python、Goの実行可能なサンプルはsolana-foundation/transaction-v1-examplesにあります。

Is this page helpful?

© 2026 Solana Foundation. 無断転載を禁じます。