Transactions and Instructions
On Solana, users send transactions to interact with the network. Transactions contain one or more instructions that specify operations to process. The execution logic for instructions are stored on programs deployed to the Solana network, where each program defines its own set of instructions.
Below are key details about Solana transaction processing:
- If a transaction includes multiple instructions, the instructions execute in the order added to the transaction.
- Transactions are "atomic" - all instructions must process successfully, or the entire transaction fails and no changes occur.
A transaction is essentially a request to process one or more instructions. You can think of a transaction as an envelope containing forms. Each form is an instruction that tells the network what to do. Sending the transaction is like mailing the envelope to get the forms processed.
Transaction Simplified
Key Points
- Solana transactions include instructions that invoke programs on the network.
- Transactions are atomic - if any instruction fails, the entire transaction fails and no changes occur.
- Instructions in a transaction execute in sequential order.
- The transaction size limit is 1232 bytes.
- Each instruction requires three pieces of information:
- The address of the program to invoke
- The accounts the instruction reads from or writes to
- Any extra data required by the instruction (e.g., function arguments)
SOL Transfer Example
The diagram below represents a transaction with a single instruction to transfer SOL from a sender to a receiver.
On Solana, "wallets" are accounts owned by the System Program. Only the program owner can change an account's data, so transferring SOL requires sending a transaction to invoke the System Program.
SOL Transfer
The sender account must sign (is_signer
) the transaction to let the System
Program deduct its lamport balance. The sender and recipient accounts must be
writable (is_writable
) since their lamport balances change.
After sending the transaction, the System Program processes the transfer instruction. The System Program then updates the lamport balances of both the sender and recipient accounts.
SOL Transfer Process
The examples below show how to send a transaction that transfers SOL from one account to another. See the System Program's transfer instruction source code here.
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);
Client libraries often abstract the details for building program instructions. If a library isn't available, you can manually build the instruction. This requires you to know the implementation details of the instruction.
The examples below show how to manually build the transfer instruction. The
Expanded Instruction
tab is functionally equivalent to the Instruction
tab.
const transferAmount = 0.01; // 0.01 SOLconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount * LAMPORTS_PER_SOL});
In the sections below, we'll walk through the details of transactions and instructions.
Instructions
An instruction on Solana program can be thought of as a public function that can be called by anyone using the Solana network.
You can think of a Solana program as a web server hosted on the Solana network,
where each instruction is like a public API endpoint that users can call to
perform specific actions. Invoking an instruction is similar to sending a POST
request to an API endpoint, allowing users to execute the program’s business
logic.
To call a program's instruction on Solana, you need to construct an
Instruction
with three pieces of information:
- Program ID: The address of the program with the business logic for the instruction being invoked.
- Accounts: The list of all accounts the instruction reads from or writes to.
- Instruction Data: A byte array specifying which instruction to invoke on the program and any arguments required by the 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>,}
Transaction Instruction
AccountMeta
When creating an Instruction
, you must provide each required account as an
AccountMeta
.
The AccountMeta
specifies the following:
- pubkey: The address of the account
- is_signer: Whether the account must sign the transaction
- is_writable: Whether the instruction modifies the account's data
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,}
By specifying up front which accounts an instruction reads or writes, transactions that don't modify the same accounts can execute in parallel.
To know which accounts an instruction requires, including which must be writable, read-only, or sign the transaction, you must refer to the implementation of the instruction as defined by the program.
In practice, you usually don’t have to construct an Instruction
manually. Most
programs developers provide client libraries with helper functions that create
the instructions for you.
AccountMeta
Example Instruction Structure
Run the examples below to see the structure of a SOL transfer instruction.
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));
The following examples show the output from the previous code snippets. The exact format differs depending on the SDK, but every Solana instruction requires the following information:
- Program ID: The address of the program that will execute the instruction.
- Accounts: A list of accounts required by the instruction. For each account, the instruction must specify its address, whether it must sign the transaction, and whether it will be written to.
- Data: A byte buffer that tells the program which instruction to execute and includes any arguments required by the 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}}
Transactions
After you have created the instructions you want to invoke, the next step is to
create a Transaction
and add the instructions to the transaction. A Solana
transaction
is made up of:
- Signatures: An array of
signatures
from all accounts required as signers for the instructions in the
transaction. A signature is created by signing the transaction
Message
with the account's private key. - Message: The transaction message includes the list of instructions to be processed atomically.
pub struct Transaction {#[wasm_bindgen(skip)]#[serde(with = "short_vec")]pub signatures: Vec<Signature>,#[wasm_bindgen(skip)]pub message: Message,}
Transaction Format
The structure of a transaction message consists of:
- Message Header: Specifies the number of signer and read-only account.
- Account Addresses: An array of account addresses required by the instructions on the transaction.
- Recent Blockhash: Acts as a timestamp for the transaction.
- Instructions: An array of instructions to be executed.
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>,}
Transaction Size
Solana transactions have a size limit of 1232 bytes. This limit comes from the IPv6 Maximum Transmission Unit (MTU) size of 1280 bytes, minus 48 bytes for network headers (40 bytes IPv6 + 8 bytes header).
A transaction's total size (signatures and message) must stay under this limit and includes:
- Signatures: 64 bytes each
- Message: Header (3 bytes), account keys (32 bytes each), recent blockhash (32 bytes), and instructions
Transaction Format
Message Header
The message header specifies the permissions for the account in the transaction. It works in combination with the strictly ordered account addresses to determine which accounts are signers and which are writable.
- The number of signatures required for all instructions on the transaction.
- The number of signed accounts that are read-only.
- The number of unsigned accounts that are read-only.
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,}
Message Header
Compact-Array Format
A compact array in a transaction message is an array serialized in the following format:
- The array length (encoded as compact-u16)
- The array items listed one after another
Compact array format
This format is used to encode the lengths of the Account Addresses and Instructions arrays in transaction messages.
Array of Account Addresses
A transaction message contains a single list of all account addresses required by its instructions. The array starts with a compact-u16 number indicating how many addresses it contains.
To save space, the transaction does not store permissions for each account
individually. Instead, it relies on a combination of the MessageHeader
and a
strict ordering of the account addresses to determine permissions.
The addresses are always ordered in the following way:
- Accounts that are writable and signers
- Accounts that are read-only and signers
- Accounts that are writable and not signers
- Accounts that are read-only and not signers
The MessageHeader
provides the values used to determine the number of accounts
for each permission group.
Compact array of account addresses
Recent Blockhash
Every transaction requires a recent blockhash that serves two purposes:
- Acts as a timestamp for when the transaction was created
- Prevents duplicate transactions
A blockhash expires after 150 blocks (about 1 minute assuming 400ms block times), after which the transaction is considered expired and cannot be processed.
You can use the getLatestBlockhash
RPC
method to get the current blockhash and last block height at which the blockhash
will be valid.
Array of Instructions
A transaction message contains an array of instructions in the CompiledInstruction type. Instructions are converted to this type when added to a transaction.
Like the account addresses array in the message, it starts with a compact-u16 length followed by the instruction data. Each instruction contains:
- Program ID Index: An index that points to the program's address in the account addresses array. This specifies the program that processes the instruction.
- Account Indexes: An array of indexes that point to the account addresses required for this instruction.
- Instruction Data: A byte array that specifies which instruction to invoke on the program and any additional data required by the instruction (eg. function arguments).
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>,}
Compact array of Instructions
Example Transaction Structure
Run the examples below to see the structure of a transaction with a single SOL transfer 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 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));
The following examples show the transaction message output from the previous code snippets. The exact format differs depending on the SDK, but includes the same information.
{"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}}]}
After submitting a transaction, you can retrieve its details using the getTransaction RPC method. The response will have a structure similar to the following snippet. Alternatively, you can inspect the transaction using Solana Explorer.
A "transaction signature" uniquely identifies a transaction on Solana. You use this signature to look up the transaction's details on the network. The transaction signature is simply the first signature on the transaction. Note that the first signature is also the signature of the transaction fee payer.
{"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?