トランザクションと命令
Solanaでは、ユーザーはネットワークと対話するためにトランザクションを送信します。トランザクションには、処理する操作を指定する1つ以上の命令が含まれています。命令の実行ロジックはSolanaネットワークにデプロイされたプログラムに格納されており、各プログラムは独自の命令セットを定義しています。
以下はSolanaトランザクション処理に関する重要な詳細です:
- トランザクションに複数の命令が含まれる場合、命令はトランザクションに追加された順序で実行されます。
- トランザクションは「アトミック」です - すべての命令が正常に処理されなければ、トランザクション全体が失敗し、変更は一切発生しません。
トランザクションは本質的に、1つ以上の命令を処理するためのリクエストです。
トランザクション簡略図
トランザクションは、フォームが入った封筒のようなものです。各フォームは、ネットワークに何をすべきかを指示する命令です。トランザクションを送信することは、フォームを処理してもらうために封筒を郵送するようなものです。
重要なポイント
- Solanaトランザクションには、ネットワーク上のプログラムを呼び出す命令が含まれています。
- トランザクションはアトミックです - いずれかの命令が失敗すると、トランザクション全体が失敗し、変更は一切発生しません。
- トランザクション内の命令は順番に実行されます。
- トランザクションのサイズ制限は1232バイトです。
- 各命令には3つの情報が必要です:
- 呼び出すプログラムのアドレス
- 命令が読み取りまたは書き込むアカウント
- 命令に必要な追加データ(例:関数の引数)
SOL送金の例
下の図は、送信者から受信者にSOLを転送する単一の命令を持つトランザクションを表しています。
Solanaでは、「ウォレット」はSystem Programが所有するアカウントです。アカウントのデータを変更できるのはプログラム所有者のみであるため、SOLを転送するにはSystem Programを呼び出すトランザクションを送信する必要があります。
SOL転送
送信者アカウントはトランザクションに署名(is_signer
)して、System
Programがlamport残高を減少させることを許可する必要があります。送信者と受信者のアカウントはlamport残高が変更されるため、書き込み可能(is_writable
)である必要があります。
トランザクションを送信した後、System Programは転送命令を処理します。その後、System Programは送信者と受信者の両方のアカウントのlamport残高を更新します。
SOL転送プロセス
以下の例は、あるアカウントから別のアカウントにSOLを転送するトランザクションを送信する方法を示しています。
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 clusterconst rpc = createSolanaRpc("http://localhost:8899");const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");// Generate sender and recipient keypairsconst 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 airdropawait airdropFactory({ rpc, rpcSubscriptions })({recipientAddress: sender.address,lamports: lamports(LAMPORTS_PER_SOL), // 1 SOLcommitment: "confirmed"});// Check balance before transferconst { 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 recipientconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount // 0.01 SOL in lamports});// Add the transfer instruction to a new transactionconst { 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 networkconst signedTransaction =await signTransactionMessageWithSigners(transactionMessage);await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction,{ commitment: "confirmed" });const transactionSignature = getSignatureFromTransaction(signedTransaction);// Check balance after transferconst { 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);
クライアントライブラリは、プログラム命令の構築の詳細を抽象化することがよくあります。ライブラリが利用できない場合は、手動で命令を構築することができます。これには命令の実装詳細を知る必要があります。
以下の例は、転送命令を手動で構築する方法を示しています。Expanded Instruction
タブはInstruction
タブと機能的に同等です。
- Kit
const transferAmount = 0.01; // 0.01 SOLconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount * LAMPORTS_PER_SOL});
- Legacy
const transferAmount = 0.01; // 0.01 SOLconst transferInstruction = SystemProgram.transfer({fromPubkey: sender.publicKey,toPubkey: receiver.publicKey,lamports: transferAmount * LAMPORTS_PER_SOL});
- Rust
let transfer_amount = LAMPORTS_PER_SOL / 100; // 0.01 SOLlet transfer_instruction =system_instruction::transfer(&sender.pubkey(), &recipient.pubkey(), transfer_amount);
以下のセクションでは、トランザクションとインストラクションの詳細について説明します。
インストラクション
Solana プログラム 上のインストラクションは、Solanaネットワークを使用して誰でも呼び出せるパブリック関数と考えることができます。
プログラムのインストラクションを呼び出すには、次の3つの重要な情報が必要です:
- プログラムID:インストラクションの実行ロジックを持つプログラム
- アカウント:インストラクションが必要とするアカウントのリスト
- instruction data:プログラム上で呼び出すインストラクションとそのインストラクションが必要とする引数を指定するバイト配列
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>,}
トランザクションインストラクション
AccountMeta
インストラクションが必要とする各アカウントは、以下を含むAccountMetaとして提供する必要があります:
pubkey
:アカウントのアドレスis_signer
:アカウントがトランザクションに署名する必要があるかどうかis_writable
:インストラクションがアカウントのデータを変更するかどうか
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,}
AccountMeta
インストラクションが読み取りまたは書き込みするアカウントを事前に指定することで、同じアカウントを変更しないトランザクションは並行して実行できます。
インストラクション構造の例
以下の例を実行して、SOL送金インストラクションの構造を確認してください。
import { generateKeyPairSigner, lamports } from "@solana/kit";import { getTransferSolInstruction } from "@solana-program/system";// Generate sender and recipient keypairsconst sender = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();// Define the amount to transferconst 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 recipientconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount});console.log(JSON.stringify(transferInstruction, null, 2));
以下の例は、前のコードスニペットからの出力を示しています。正確な形式はSDKによって異なりますが、すべてのSolanaインストラクションには次の情報が必要です:
- プログラムID: 命令を実行するプログラムのアドレス。
- アカウント: 命令に必要なアカウントのリスト。各アカウントについて、命令はそのアドレス、トランザクションに署名する必要があるかどうか、および書き込まれるかどうかを指定する必要があります。
- データ: プログラムにどの命令を実行するかを伝え、命令に必要な引数を含むバイトバッファ。
{"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}}
トランザクション
Solanaのトランザクションは以下で構成されています:
pub struct Transaction {#[wasm_bindgen(skip)]#[serde(with = "short_vec")]pub signatures: Vec<Signature>,#[wasm_bindgen(skip)]pub 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>,}
トランザクションメッセージ
トランザクションサイズ
Solanaトランザクションのサイズ制限は1232バイトです。この制限はIPv6の最大伝送単位(MTU)サイズ1280バイトから、ネットワークヘッダー用の48バイト(IPv6用40バイト + フラグメントヘッダー用8バイト)を引いたものです。
トランザクションの総サイズ(署名とメッセージ)はこの制限を下回る必要があり、以下を含みます:
- 署名:各64バイト
- メッセージ:ヘッダー(3バイト)、アカウントキー(各32バイト)、最近のブロックハッシュ(32バイト)、および命令
トランザクションフォーマット
メッセージヘッダー
メッセージヘッダーは3バイトを使用してアカウント権限を定義します。
- 必要な署名
- 読み取り専用署名済みアカウントの数
- 読み取り専用未署名アカウントの数
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,}
メッセージヘッダー
コンパクト配列フォーマット
トランザクションメッセージ内のコンパクト配列は、以下のフォーマットでシリアライズされた配列です:
- 配列の長さ(compact-u16としてエンコード)
- 配列の項目が一つずつ連続して並ぶ
コンパクト配列フォーマット
このフォーマットは、トランザクションメッセージ内のアカウントアドレスとinstruction data配列の長さをエンコードするために使用されます。
アカウントアドレスの配列
トランザクションメッセージには、そのinstructionが必要とするアカウントアドレスの配列が含まれています。配列は、含まれるアドレスの数を示すcompact-u16から始まります。アドレスはメッセージヘッダーによって決定される権限によって順序付けられています。
- 書き込み可能で署名者であるアカウント
- 読み取り専用で署名者であるアカウント
- 書き込み可能で署名者でないアカウント
- 読み取り専用で署名者でないアカウント
アカウントアドレスのコンパクト配列
最近のブロックハッシュ
すべてのトランザクションには、次の2つの目的を果たす最近のブロックハッシュが必要です:
- タイムスタンプとして機能する
- 重複トランザクションを防止する
ブロックハッシュは150ブロック(400msのブロック時間を想定すると約1分)後に期限切れとなり、その後トランザクションは処理できなくなります。
getLatestBlockhash RPCメソッドを使用して、現在のブロックハッシュとブロックハッシュが有効な最後のブロック高を取得できます。Solana Playgroundの例をご覧ください。
instructionの配列
トランザクションメッセージには、CompiledInstruction型のinstruction配列が含まれています。instructionはトランザクションに追加されるときにこの型に変換されます。
メッセージ内のアカウントアドレス配列と同様に、compact-u16の長さから始まり、その後にinstruction dataが続きます。各instructionには以下が含まれます:
- プログラムIDインデックス:アカウントアドレス配列内のプログラムのアドレスを指すu8インデックスです。これは命令を処理するプログラムを指定します。
- アカウントインデックス:この命令に必要なアカウントアドレスを指すu8インデックスの配列です。
- instruction data:プログラム上で呼び出す命令と、命令に必要な追加データ(例:関数の引数)を指定するバイト配列です。
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>,}
命令のコンパクト配列
トランザクション構造の例
以下の例を実行して、単一のSOL送金命令を持つトランザクションの構造を確認してください。
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 keypairsconst sender = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();// Define the amount to transferconst 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 recipientconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount});// Create transaction messageconst transactionMessage = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),(tx) => appendTransactionMessageInstructions([transferInstruction], tx));const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);// Decode the messageBytesconst compiledTransactionMessage =getCompiledTransactionMessageDecoder().decode(signedTransaction.messageBytes);console.log(JSON.stringify(compiledTransactionMessage, null, 2));
以下の例は、前のコードスニペットからのトランザクションメッセージの出力を示しています。正確な形式は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}}]}
トランザクションをネットワークに送信した後、そのシグネチャを使用してトランザクションを取得すると、以下の構造のレスポンスが返されます。
message
フィールドには以下のフィールドが含まれています:
-
header
:accountKeys
配列内のアドレスに対する読み取り/書き込みおよび署名者の権限を指定します -
accountKeys
:トランザクションの命令で使用されるすべてのアカウントアドレスの配列 -
recentBlockhash
:トランザクションのタイムスタンプに使用されるブロックハッシュ -
instructions
:実行する命令の配列。各命令のaccount
とprogramIdIndex
は、インデックスによってaccountKeys
配列を参照します。 -
signatures
:トランザクションの命令によって署名者として必要とされるすべてのアカウントの署名を含む配列。署名は、アカウントの対応する秘密鍵を使用してトランザクションメッセージに署名することで作成されます。
{"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?