トランザクションとインストラクション

Solanaでは、ユーザーはネットワークとやり取りするためにトランザクションを送信します。トランザクションには、処理する操作を指定する1つ以上のインストラクションが含まれています。インストラクションの実行ロジックは、Solanaネットワークにデプロイされたプログラムに格納されており、各プログラムは独自のインストラクションセットを定義しています。

以下はSolanaトランザクション処理に関する重要な詳細です:

  • トランザクションに複数のインストラクションが含まれる場合、インストラクションはトランザクションに追加された順序で実行されます。
  • トランザクションは「アトミック」です - すべてのインストラクションが正常に処理されなければ、トランザクション全体が失敗し、変更は一切発生しません。

トランザクションは本質的に、1つ以上のinstructionsを処理するリクエストです。トランザクションは封筒に入った書類のようなものと考えることができます。各書類はinstructionであり、ネットワークに何をすべきかを指示します。トランザクションを送信することは、その封筒を処理してもらうために郵送するようなものです。

トランザクション簡略図トランザクション簡略図

重要なポイント

  • Solanaトランザクションには、ネットワーク上のプログラムを呼び出すinstructionsが含まれています。
  • トランザクションはアトミックです - いずれかのinstructionが失敗すると、トランザクション全体が失敗し、変更は一切発生しません。
  • トランザクション内のinstructionsは順番に実行されます。
  • トランザクションのサイズ制限は1232バイトです。
  • 各instructionには次の3つの情報が必要です:
    1. 呼び出すプログラムのアドレス
    2. instructionが読み取りまたは書き込むアカウント
    3. instruction実行に必要な追加データ(例:関数の引数)

SOL送金の例

以下の図は、送信者から受信者にSOLを送金するための単一のinstructionを持つトランザクションを表しています。

Solanaでは、「ウォレット」はSystem Programが所有するアカウントです。アカウントのデータを変更できるのはプログラム所有者のみであるため、SOLを送金するにはSystem Programを呼び出すトランザクションを送信する必要があります。

SOL送金SOL送金

送信者アカウントはトランザクションに署名(is_signer)する必要があり、これによりSystem Programがlamport残高を減少させることができます。送信者と受信者のアカウントはlamport残高が変更されるため、書き込み可能(is_writable)である必要があります。

トランザクションを送信すると、System Programは送金instructionを処理します。その後、System Programは送信者と受信者の両方のアカウントのlamport残高を更新します。

SOL送金プロセスSOL送金プロセス

以下の例は、あるアカウントから別のアカウントにSOLを送金するトランザクションを送信する方法を示しています。System Programの送金instruction用ソースコードはこちらをご覧ください。

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.

クライアントライブラリは、プログラムinstructionsの構築の詳細を抽象化することがよくあります。ライブラリが利用できない場合は、手動でinstructionを構築することができます。これにはinstructionの実装詳細を知る必要があります。

以下の例は、転送instructionを手動で構築する方法を示しています。 Expanded Instruction タブは機能的に Instruction タブと同等です。

const transferAmount = 0.01; // 0.01 SOL
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount * LAMPORTS_PER_SOL
});

以下のセクションでは、トランザクションとinstructionsの詳細について説明します。

Instructions

Solanaのプログラム上のinstructionは、Solanaネットワークを使用して誰でも呼び出すことができる公開関数と考えることができます。

Solanaプログラムは、Solanaネットワーク上でホストされるWebサーバーのようなものと考えることができます。各instructionは、ユーザーが特定のアクションを実行するために呼び出せる公開APIエンドポイントのようなものです。instructionを呼び出すことは、APIエンドポイントにPOSTリクエストを送信するのに似ており、ユーザーがプログラムのビジネスロジックを実行できるようにします。

Solana上でプログラムのinstructionを呼び出すには、3つの情報を含むInstructionを構築する必要があります:

  • プログラムID:呼び出されるinstructionのビジネスロジックを持つプログラムのアドレス。
  • アカウント:instructionが読み取りまたは書き込みを行うすべてのアカウントのリスト。
  • Instruction Data:プログラム上で呼び出すinstructionと、instructionに必要な引数を指定するバイト配列。
Instruction
pub struct Instruction {
/// Pubkey of the program that executes this instruction.
pub program_id: Pubkey,
/// Metadata describing accounts that should be passed to the program.
pub accounts: Vec<AccountMeta>,
/// Opaque data passed to the program for its own interpretation.
pub data: Vec<u8>,
}

トランザクションInstructionトランザクションInstruction

AccountMeta

Instructionを作成する際、必要な各アカウントをAccountMetaとして提供する必要があります。AccountMetaは以下を指定します:

  • pubkey:アカウントのアドレス
  • is_signer:アカウントがトランザクションに署名する必要があるかどうか
  • is_writable:instructionがアカウントのデータを変更するかどうか
AccountMeta
pub struct AccountMeta {
/// An account's public key.
pub pubkey: Pubkey,
/// True if an `Instruction` requires a `Transaction` signature matching `pubkey`.
pub is_signer: bool,
/// True if the account data or metadata may be mutated during program execution.
pub is_writable: bool,
}

instructionが読み取りまたは書き込みを行うアカウントを事前に指定することで、同じアカウントを変更しないトランザクションは並行して実行できます。

instructionが必要とするアカウント(書き込み可能、読み取り専用、またはトランザクションに署名する必要があるものを含む)を知るには、プログラムによって定義されたinstructionの実装を参照する必要があります。

実際には、通常は手動で「instruction」を構築する必要はありません。ほとんどのプログラム開発者は、instructionを作成するためのヘルパー関数を含むクライアントライブラリを提供しています。

AccountMetaAccountMeta

Instruction構造の例

以下の例を実行して、SOL送金instructionの構造を確認してください。

import { generateKeyPairSigner, lamports } from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
// 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
});
console.log(JSON.stringify(transferInstruction, null, 2));
Console
Click to execute the code.

以下の例は、前述のコードスニペットからの出力を示しています。正確な形式はSDKによって異なりますが、すべてのSolana instructionには以下の情報が必要です:

  • プログラムID:instructionを実行するプログラムのアドレス。
  • アカウント:instructionが必要とするアカウントのリスト。各アカウントについて、instructionはそのアドレス、トランザクションに署名する必要があるかどうか、および書き込まれるかどうかを指定する必要があります。
  • データ:プログラムに実行するinstructionを伝え、instructionが必要とする引数を含むバイトバッファ。
{
"accounts": [
{
"address": "Hu28vRMGWpQXN56eaE7jRiDDRRz3vCXEs7EKHRfL6bC",
"role": 3,
"signer": {
"address": "Hu28vRMGWpQXN56eaE7jRiDDRRz3vCXEs7EKHRfL6bC",
"keyPair": {
"privateKey": {},
"publicKey": {}
}
}
},
{
"address": "2mBY6CTgeyJNJDzo6d2Umipw2aGUquUA7hLdFttNEj7p",
"role": 1
}
],
"programAddress": "11111111111111111111111111111111",
"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
}
}

トランザクション

実行したいinstructionを作成した後、次のステップは「Transaction」を作成し、instructionをトランザクションに追加することです。Solanaのトランザクションは以下で構成されています:

  1. 署名:トランザクション内のinstructionに必要な署名者からのすべてのアカウントの署名の配列。署名は、アカウントの秘密鍵でトランザクション「message」に署名することで作成されます。
  2. メッセージ:トランザクションメッセージには、アトミックに処理されるinstructionのリストが含まれています。
Transaction
pub struct Transaction {
#[wasm_bindgen(skip)]
#[serde(with = "short_vec")]
pub signatures: Vec<Signature>,
#[wasm_bindgen(skip)]
pub message: Message,
}

トランザクションフォーマットトランザクションフォーマット

トランザクションメッセージの構造は以下で構成されています:

  • メッセージヘッダー:署名者と読み取り専用アカウントの数を指定します。
  • アカウントアドレス:トランザクション上のinstructionsに必要なアカウントアドレスの配列。
  • 最近のブロックハッシュ:トランザクションのタイムスタンプとして機能します。
  • Instructions:実行されるinstructionsの配列。
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>,
}

トランザクションサイズ

Solanaトランザクションのサイズ制限は1232バイトです。この制限はIPv6の最大伝送単位(MTU)サイズ1280バイトから、ネットワークヘッダー用の48バイト(IPv6の40バイト + ヘッダーの8バイト)を引いたものです。

トランザクションの総サイズ(署名とメッセージ)はこの制限以下に収まる必要があり、以下を含みます:

  • 署名:各64バイト
  • メッセージ:ヘッダー(3バイト)、アカウントキー(各32バイト)、最近のブロックハッシュ(32バイト)、およびinstructions

トランザクションフォーマットトランザクションフォーマット

メッセージヘッダー

メッセージヘッダーはトランザクション内のアカウントの権限を指定します。厳密に順序付けられたアカウントアドレスと組み合わせて機能し、どのアカウントが署名者であり、どのアカウントが書き込み可能かを決定します。

  1. トランザクション上のすべてのinstructionsに必要な署名の数。
  2. 読み取り専用の署名済みアカウントの数。
  3. 読み取り専用の未署名アカウントの数。
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,
}

メッセージヘッダーメッセージヘッダー

コンパクト配列フォーマット

トランザクションメッセージ内のコンパクト配列は、以下のフォーマットでシリアル化された配列です:

  1. 配列の長さ(compact-u16としてエンコード)
  2. 配列項目が一つずつ連続して並べられる

コンパクト配列フォーマットコンパクト配列フォーマット

このフォーマットは、トランザクションメッセージ内のアカウントアドレスInstructions配列の長さをエンコードするために使用されます。

アカウントアドレスの配列

トランザクションメッセージには、そのinstructionsに必要なすべての アカウントアドレス の単一リストが含まれています。配列は compact-u16形式の数値で始まり、含まれるアドレスの数を示します。

スペースを節約するため、トランザクションは各アカウントの権限を個別に保存しません。代わりに、MessageHeaderとアカウントアドレスの厳格な順序付けの組み合わせに依存して権限を決定します。

アドレスは常に次の順序で並べられます:

  1. 書き込み可能かつ署名者であるアカウント
  2. 読み取り専用かつ署名者であるアカウント
  3. 書き込み可能だが署名者ではないアカウント
  4. 読み取り専用かつ署名者ではないアカウント

MessageHeaderは、各権限グループのアカウント数を決定するために使用される値を提供します。

アカウントアドレスのコンパクト配列アカウントアドレスのコンパクト配列

最近のブロックハッシュ

すべてのトランザクションには 最近のブロックハッシュ が必要であり、これは2つの目的を果たします:

  1. トランザクションが作成された時間のタイムスタンプとして機能する
  2. 重複トランザクションを防止する

ブロックハッシュは 150 ブロック(400msのブロック時間を想定すると約1分)後に期限切れとなり、その後トランザクションは期限切れと見なされ処理できなくなります。

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

instructionsの配列

トランザクションメッセージには CompiledInstruction 型の instructions 配列が含まれています。instructionsはトランザクションに追加されるときにこの型に変換されます。

メッセージ内のアカウントアドレス配列と同様に、 compact-u16形式の長さで始まり、その後にinstruction dataが続きます。各instructionには以下が含まれます:

  1. プログラムIDインデックス: アカウントアドレス配列内のプログラムのアドレスを指すインデックス。これはinstructionを処理するプログラムを指定します。
  2. アカウントインデックス: このinstructionに必要なアカウントアドレスを指すインデックスの配列。
  3. instruction data: プログラム上で呼び出すinstructionと、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のコンパクト配列

トランザクション構造の例

以下の例を実行して、単一の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を使用してトランザクションを調査することもできます。

「トランザクション署名」はSolana上のトランザクションを一意に識別します。この署名を使用してネットワーク上のトランザクションの詳細を検索できます。トランザクション署名は単にトランザクションの最初の署名です。最初の署名はトランザクション手数料支払者の署名でもあることに注意してください。

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?