トランザクション構造

概要

トランザクションは署名とメッセージで構成されます。メッセージにはヘッダー、アカウントアドレス、最新のブロックハッシュ、コンパイル済みinstructionsが含まれます。シリアライズ後の最大サイズ:1,232バイト。

Transactionには、2つのトップレベルフィールドがあります:

  • signatures: 署名の配列
  • message: 処理されるinstructionsのリストを含むトランザクション情報
Transaction
pub struct Transaction {
pub signatures: Vec<Signature>,
pub message: Message,
}

トランザクションの2つの部分を示す図トランザクションの2つの部分を示す図

トランザクションのシリアライズ後の合計サイズは、PACKET_DATA_SIZE(1,232バイト)を超えてはなりません。この制限は、1,280バイト(IPv6の最小MTU)からネットワークヘッダー用の48バイト(IPv6用40バイト + フラグメントヘッダー用8バイト)を引いた値です。1,232バイトには、signatures配列とmessage構造体の両方が含まれます。

トランザクション形式とサイズ制限を示す図トランザクション形式とサイズ制限を示す図

署名

signaturesフィールドは、Signature値のコンパクトエンコード配列です。各Signatureは、署名者アカウントの秘密鍵で署名された、シリアライズされたMessageの64バイトEd25519署名です。トランザクションのinstructionsによって参照される署名者アカウントごとに1つの署名が必要です。

配列の最初の署名は手数料支払者のものであり、トランザクションの基本手数料と優先手数料を支払うアカウントです。この最初の署名はトランザクションIDとしても機能し、ネットワーク上でトランザクションを検索するために使用されます。トランザクションIDは一般的にトランザクション署名と呼ばれます。

手数料支払者の要件:

  • メッセージの最初のアカウント(インデックス0)であり、署名者である必要があります。
  • システムプログラムが所有するアカウント、またはnonceアカウントである必要があります(validate_fee_payerで検証されます)。
  • rent_exempt_minimum + total_feeをカバーするのに十分なlamportsを保持している必要があります。そうでない場合、トランザクションは*rsInsufficientFundsForFee*で失敗します。

メッセージ

messageフィールドは、トランザクションのペイロードを含むMessage構造体です:

Message
pub struct Message {
/// The message header, identifying signed and read-only `account_keys`.
pub header: MessageHeader,
/// All the account keys used by this transaction.
#[serde(with = "short_vec")]
pub account_keys: Vec<Pubkey>,
/// The id of a recent ledger entry.
pub recent_blockhash: Hash,
/// Programs that will be executed in sequence and committed in
/// one atomic transaction if all succeed.
#[serde(with = "short_vec")]
pub instructions: Vec<CompiledInstruction>,
}

ヘッダー

headerフィールドは、account_keys配列を権限グループに分割する3つのu8フィールドを持つMessageHeader構造体です:

  • num_required_signatures: トランザクションに必要な署名の総数。
  • num_readonly_signed_accounts: 読み取り専用の署名済みアカウントの数。
  • num_readonly_unsigned_accounts: 読み取り専用の未署名アカウントの数。
MessageHeader
pub struct MessageHeader {
/// The number of signatures required for this message to be considered
/// valid. The signers of those signatures must match the first
/// `num_required_signatures` of [`Message::account_keys`].
pub num_required_signatures: u8,
/// The last `num_readonly_signed_accounts` of the signed keys are read-only
/// accounts.
pub num_readonly_signed_accounts: u8,
/// The last `num_readonly_unsigned_accounts` of the unsigned keys are
/// read-only accounts.
pub num_readonly_unsigned_accounts: u8,
}

メッセージヘッダーの3つの部分を示す図メッセージヘッダーの3つの部分を示す図

アカウントアドレス

account_keysフィールドは、公開鍵のコンパクトエンコードされた配列です。各エントリは、トランザクションのinstructionsの少なくとも1つで使用されるアカウントを識別します。配列にはすべてのアカウントを含める必要があり、次の厳密な順序に従う必要があります:

  1. 署名者 + 書き込み可能
  2. 署名者 + 読み取り専用
  3. 非署名者 + 書き込み可能
  4. 非署名者 + 読み取り専用

この厳密な順序により、account_keys配列をメッセージのheader内の3つのカウントと組み合わせることで、アカウントごとのメタデータフラグを保存することなく、各アカウントの権限を決定できます。ヘッダーのカウントは、配列を上記の4つの権限グループに分割します。

アカウントアドレス配列の順序を示す図アカウントアドレス配列の順序を示す図

最新のブロックハッシュ

recent_blockhashフィールドは、2つの目的を果たす32バイトのハッシュです。

  1. タイムスタンプ: トランザクションが最近作成されたことを証明します。
  2. 重複排除: 同じトランザクションが2回処理されることを防ぎます。

ブロックハッシュは150スロット後に期限切れになります。トランザクションが到着したときにブロックハッシュが有効でない場合、durable nonceトランザクションでない限り、*rsBlockhashNotFound*で拒否されます。

getLatestBlockhash RPCメソッドを使用すると、現在のブロックハッシュと、そのブロックハッシュが有効な最後のブロック高を取得できます。

Instructions

instructionsフィールドは、CompiledInstruction構造体のコンパクトエンコードされた配列です。各*rsCompiledInstruction*は、完全な公開鍵ではなく、account_keys配列へのインデックスによってアカウントを参照します。これには以下が含まれます。

  1. program_id_index: 呼び出すプログラムを識別するaccount_keysへのインデックス。
  2. accounts: プログラムに渡すアカウントを指定するaccount_keysへのインデックスの配列。
  3. data: instruction識別子とシリアライズされた引数を含むバイト配列。
CompiledInstruction
pub struct CompiledInstruction {
/// Index into the transaction keys array indicating the program account that executes this instruction.
pub program_id_index: u8,
/// Ordered indices into the transaction keys array indicating which accounts to pass to the program.
#[serde(with = "short_vec")]
pub accounts: Vec<u8>,
/// The program input data.
#[serde(with = "short_vec")]
pub data: Vec<u8>,
}

Instructionsのコンパクト配列Instructionsのコンパクト配列

トランザクションのバイナリ形式

トランザクションは、コンパクトエンコーディング方式を使用してシリアライズされます。すべての可変長配列(署名、アカウントキー、instructions)には、compact-u16長エンコーディングが接頭辞として付けられます。この形式は、0〜127の値には1バイト、より大きな値には2〜3バイトを使用します。

レガシートランザクションのレイアウト(ワイヤー上):

フィールドサイズ説明
num_signatures1〜3バイト(compact-u16)署名の数
signaturesnum_signatures x 64バイトEd25519署名
num_required_signatures1バイトMessageHeaderフィールド1
num_readonly_signed1バイトMessageHeaderフィールド2
num_readonly_unsigned1バイトMessageHeaderフィールド3
num_account_keys1〜3バイト(compact-u16)静的アカウントキーの数
account_keysnum_account_keys x 32バイト公開鍵
recent_blockhash32バイトブロックハッシュ
num_instructions1〜3バイト(compact-u16)instructionsの数
instructions可変コンパイル済みinstructionsの配列

コンパイルされた各instructionは次のようにシリアライズされます:

フィールドサイズ説明
program_id_index1バイトアカウントキーへのインデックス
num_accounts1-3バイト (compact-u16)アカウントインデックスの数
account_indicesnum_accounts x 1バイトアカウントキーインデックス
data_len1-3バイト (compact-u16)instruction dataの長さ
datadata_len バイト不透明なinstruction data

サイズ計算

PACKET_DATA_SIZE = 1,232バイトの場合、利用可能なスペースは次のように計算できます:

Total = 1232 bytes
- compact-u16(num_sigs) # 1 byte
- num_sigs * 64 # signature bytes
- 3 # message header
- compact-u16(num_keys) # 1 byte
- num_keys * 32 # account key bytes
- 32 # recent blockhash
- compact-u16(num_ixs) # 1 byte
- sum(instruction_sizes) # per-instruction overhead + data

例: SOL転送トランザクション

以下の図は、トランザクションとinstructionsがどのように連携してユーザーがネットワークと対話できるようにするかを示しています。この例では、SOLがあるアカウントから別のアカウントに転送されます。

送信者アカウントのメタデータは、トランザクションに署名する必要があることを示しています。これにより、System Programはlamportを差し引くことができます。lamport残高を変更するには、送信者と受信者の両方のアカウントが書き込み可能である必要があります。このinstructionを実行するために、送信者のウォレットは署名とSOL転送instructionを含むメッセージを含むトランザクションを送信します。

SOL転送図SOL転送図

トランザクションが送信された後、System Programは転送instructionを処理し、両方のアカウントのlamport残高を更新します。

SOL転送プロセス図SOL転送プロセス図

以下の例は、上記の図に関連するコードを示しています。System Programのtransfer関数を参照してください。

import {
airdropFactory,
appendTransactionMessageInstructions,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
// Create a connection to cluster
const rpc = createSolanaRpc("http://localhost:8899");
const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");
// Generate sender and recipient keypairs
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Fund sender with airdrop
await airdropFactory({ rpc, rpcSubscriptions })({
recipientAddress: sender.address,
lamports: lamports(LAMPORTS_PER_SOL), // 1 SOL
commitment: "confirmed"
});
// Check balance before transfer
const { value: preBalance1 } = await rpc.getBalance(sender.address).send();
const { value: preBalance2 } = await rpc.getBalance(recipient.address).send();
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount // 0.01 SOL in lamports
});
// Add the transfer instruction to a new transaction
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(sender, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([transferInstruction], tx)
);
// Send the transaction to the network
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(
signedTransaction,
{ commitment: "confirmed" }
);
const transactionSignature = getSignatureFromTransaction(signedTransaction);
// Check balance after transfer
const { value: postBalance1 } = await rpc.getBalance(sender.address).send();
const { value: postBalance2 } = await rpc.getBalance(recipient.address).send();
console.log(
"Sender prebalance:",
Number(preBalance1) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Recipient prebalance:",
Number(preBalance2) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Sender postbalance:",
Number(postBalance1) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Recipient postbalance:",
Number(postBalance2) / Number(LAMPORTS_PER_SOL)
);
console.log("Transaction Signature:", transactionSignature);
Console
Click to execute the code.

以下の例は、単一のSOL転送instructionを含むトランザクションの構造を示しています。

import {
createSolanaRpc,
generateKeyPairSigner,
lamports,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
pipe,
signTransactionMessageWithSigners,
getCompiledTransactionMessageDecoder
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
const rpc = createSolanaRpc("http://localhost:8899");
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
// Generate sender and recipient keypairs
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
// Define the amount to transfer
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount
});
// Create transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(sender, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([transferInstruction], tx)
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
// Decode the messageBytes
const compiledTransactionMessage =
getCompiledTransactionMessageDecoder().decode(signedTransaction.messageBytes);
console.log(JSON.stringify(compiledTransactionMessage, null, 2));
Console
Click to execute the code.

以下のコードは、前のコードスニペットからの出力を示しています。 SDK間で形式は異なりますが、各インストラクションには同じ必須情報が含まれていることに注目してください。

{
"version": 0,
"header": {
"numSignerAccounts": 1,
"numReadonlySignerAccounts": 0,
"numReadonlyNonSignerAccounts": 1
},
"staticAccounts": [
"HoCy8p5xxDDYTYWEbQZasEjVNM5rxvidx8AfyqA4ywBa",
"5T388jBjovy7d8mQ3emHxMDTbUF8b7nWvAnSiP3EAdFL",
"11111111111111111111111111111111"
],
"lifetimeToken": "EGCWPUEXhqHJWYBfDirq3mHZb4qDpATmYqBZMBy9TBC1",
"instructions": [
{
"programAddressIndex": 2,
"accountIndices": [0, 1],
"data": {
"0": 2,
"1": 0,
"2": 0,
"3": 0,
"4": 128,
"5": 150,
"6": 152,
"7": 0,
"8": 0,
"9": 0,
"10": 0,
"11": 0
}
}
]
}

トランザクション詳細の取得

送信後、トランザクション署名とgetTransaction RPCメソッドを使用してトランザクション詳細を取得します。

Solana Explorerを使用してトランザクションを見つけることもできます。

Transaction Data
{
"blockTime": 1745196488,
"meta": {
"computeUnitsConsumed": 150,
"err": null,
"fee": 5000,
"innerInstructions": [],
"loadedAddresses": {
"readonly": [],
"writable": []
},
"logMessages": [
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success"
],
"postBalances": [989995000, 10000000, 1],
"postTokenBalances": [],
"preBalances": [1000000000, 0, 1],
"preTokenBalances": [],
"rewards": [],
"status": {
"Ok": null
}
},
"slot": 13049,
"transaction": {
"message": {
"header": {
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 1,
"numRequiredSignatures": 1
},
"accountKeys": [
"8PLdpLxkuv9Nt8w3XcGXvNa663LXDjSrSNon4EK7QSjQ",
"7GLg7bqgLBv1HVWXKgWAm6YoPf1LoWnyWGABbgk487Ma",
"11111111111111111111111111111111"
],
"recentBlockhash": "7ZCxc2SDhzV2bYgEQqdxTpweYJkpwshVSDtXuY7uPtjf",
"instructions": [
{
"accounts": [0, 1],
"data": "3Bxs4NN8M2Yn4TLb",
"programIdIndex": 2,
"stackHeight": null
}
],
"indexToProgramIds": {}
},
"signatures": [
"3jUKrQp1UGq5ih6FTDUUt2kkqUfoG2o4kY5T1DoVHK2tXXDLdxJSXzuJGY4JPoRivgbi45U2bc7LZfMa6C4R3szX"
]
},
"version": "legacy"
}

Is this page helpful?

目次

ページを編集

管理運営

© 2026 Solana Foundation.
無断転載を禁じます。
つながろう