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.

Transaction SimplifiedTransaction Simplified

A transaction is like 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.

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 on a transaction execute in sequential order.
  • The transaction size limit is 1232 bytes.
  • Each instruction requires three pieces of information:
    1. The address of the program to invoke
    2. The accounts the instruction reads from or writes to
    3. 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 TransferSOL 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 ProcessSOL Transfer Process

The examples below show how to send a transaction with an instruction to transfer SOL from one account to another.

Client libraries often abstract the details for building program instructions. If a library isn't available, you can manually build the instruction.

The two examples below are functionally equivalent. Example 1 uses the SystemProgram.transfer method, which abstracts the details of creating the instruction data buffer and AccountMeta for each account required by the instruction.

// Define the amount to transfer
const transferAmount = 0.01; // 0.01 SOL
// Create a transfer instruction for transferring SOL from wallet_1 to wallet_2
const transferInstruction = SystemProgram.transfer({
fromPubkey: sender.publicKey,
toPubkey: receiver.publicKey,
lamports: transferAmount * LAMPORTS_PER_SOL // Convert transferAmount to lamports
});
// Add the transfer instruction to a new transaction
const transaction = new Transaction().add(transferInstruction);
import {
LAMPORTS_PER_SOL,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
Keypair,
Connection
} from "@solana/web3.js";
// Use devnet cluster connection
const connection = new Connection("http://localhost:8899", "confirmed");
// Generate sender and recipient keypairs
const sender = Keypair.generate();
const recipient = new Keypair();
// Fund sender with airdrop
const airdropSignature = await connection.requestAirdrop(
sender.publicKey,
LAMPORTS_PER_SOL
);
await connection.confirmTransaction(airdropSignature, "confirmed");
// Check balance before transfer
const preBalance1 = await connection.getBalance(sender.publicKey);
const preBalance2 = await connection.getBalance(recipient.publicKey);
// Define the amount to transfer
const transferAmount = 0.01; // 0.01 SOL
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = SystemProgram.transfer({
fromPubkey: sender.publicKey,
toPubkey: recipient.publicKey,
lamports: transferAmount * LAMPORTS_PER_SOL // Convert transferAmount to lamports
});
// Add the transfer instruction to a new transaction
const transaction = new Transaction().add(transferInstruction);
// Send the transaction to the network
const transactionSignature = await sendAndConfirmTransaction(
connection,
transaction,
[sender] // signer
);
// Check balance after transfer
const postBalance1 = await connection.getBalance(sender.publicKey);
const postBalance2 = await connection.getBalance(recipient.publicKey);
console.log("Sender prebalance:", preBalance1 / LAMPORTS_PER_SOL);
console.log("Recipient prebalance:", preBalance2 / LAMPORTS_PER_SOL);
console.log("Sender postbalance:", postBalance1 / LAMPORTS_PER_SOL);
console.log("Recipient postbalance:", postBalance2 / LAMPORTS_PER_SOL);
console.log("Transaction Signature:", transactionSignature);

Click 'Run' to see output.
Program Logs in the Output are clickable links to Solana Explorer.
You must enable the "Enable Custom URL Param" setting on Solana Explorer.
If not enabled, links will default to localhost:8899 instead of the Mirror.ad RPC URL.

In the sections below, we'll walk through the details of transactions and instructions.

Instructions

An instruction on a Solana program can be thought of as a public function that can be called by anyone using the Solana network.

Invoking a program's instruction requires three key pieces of information:

  • Program ID: The program with the execution logic for the instruction
  • Accounts: List of accounts the instruction needs
  • Instruction Data: Byte array specifying the instruction to invoke on the program and any arguments required by the 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>,
}

Transaction InstructionTransaction Instruction

AccountMeta

Each account required by an instruction must be provided as an AccountMeta that contains:

  • pubkey: Account's address
  • is_signer: Whether the account must sign the transaction
  • is_writable: Whether the instruction modifies the account's data
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,
}

AccountMetaAccountMeta

By specifying up front which accounts an instruction reads or writes, transactions that don't modify the same accounts can execute in parallel.

Example Instruction Structure

The example below shows the structure of a SOL transfer instruction.

  • keys: Includes the AccountMeta for each account required by an instruction.
  • programId: The address of the program with the execution logic for the instruction.
  • data: The instruction data for the instruction as a buffer of bytes
SOL Transfer Instruction
{
"keys": [
{
"pubkey": "3z9vL1zjN6qyAFHhHQdWYRTFAcy69pJydkZmSFBKHg1R",
"isSigner": true,
"isWritable": true
},
{
"pubkey": "BpvxsLYKQZTH42jjtWHZpsVSa7s6JVwLKwBptPSHXuZc",
"isSigner": false,
"isWritable": true
}
],
"programId": "11111111111111111111111111111111",
"data": [2, 0, 0, 0, 128, 150, 152, 0, 0, 0, 0, 0]
}
import {
LAMPORTS_PER_SOL,
SystemProgram,
Transaction,
Keypair,
Connection
} from "@solana/web3.js";
const connection = new Connection("http://localhost:8899", "confirmed");
// Generate sender and recipient keypairs
const sender = Keypair.generate();
const recipient = new Keypair();
// Define the amount to transfer
const transferAmount = 0.01; // 0.01 SOL
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = SystemProgram.transfer({
fromPubkey: sender.publicKey,
toPubkey: recipient.publicKey,
lamports: transferAmount * LAMPORTS_PER_SOL // Convert transferAmount to lamports
});
console.log(JSON.stringify(transferInstruction, null, 2));

Click 'Run' to see output.
Program Logs in the Output are clickable links to Solana Explorer.
You must enable the "Enable Custom URL Param" setting on Solana Explorer.
If not enabled, links will default to localhost:8899 instead of the Mirror.ad RPC URL.

Transactions

A Solana transaction consists of:

  1. Signatures: An array of signatures included on the transaction.
  2. Message: List of instructions to be processed atomically.
Transaction
pub struct Transaction {
#[wasm_bindgen(skip)]
#[serde(with = "short_vec")]
pub signatures: Vec<Signature>,
#[wasm_bindgen(skip)]
pub message: Message,
}

Transaction FormatTransaction Format

The structure of a transaction message consists of:

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>,
}

Transaction MessageTransaction Message

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 fragment 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 FormatTransaction Format

Message Header

The message header uses three bytes to define account privileges.

  1. Required signatures
  2. Number of read-only signed accounts
  3. Number of read-only 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,
}

Message HeaderMessage Header

Compact-Array Format

A compact array in a transaction message is an array serialized in the following format:

  1. The array length (encoded as compact-u16)
  2. The array items listed one after another

Compact array formatCompact 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 an array of account addresses required by its instructions. The array starts with a compact-u16 number indicating how many addresses it contains. The addresses are then ordered by their privileges, as determined by the message header.

  • 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

Compact array of account addressesCompact array of account addresses

Recent Blockhash

Every transaction requires a recent blockhash that serves two purposes:

  1. Acts as a timestamp
  2. Prevents duplicate transactions

A blockhash expires after 150 blocks (about 1 minute assuming 400ms block times), after which the transaction 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. Here is an example on Solana Playground.

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:

  1. Program ID Index: An u8 index that points to the program's address in the account addresses array. This specifies the program that will process the instruction.
  2. Account Indexes: An array of u8 indexes that point to the account addresses required for this instruction.
  3. Instruction Data: A byte array specifying which instruction to invoke on the program and any additional data required by the instruction (eg. function arguments).
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>,
}

Compact array of InstructionsCompact array of Instructions

Example Transaction Structure

The example below shows the structure of a transaction with a single SOL transfer instruction before it has been sent to the network.

Transaction Data
{
"recentBlockhash": "BVSg5fnMhWXHtAV5631CMCkvAsqkJWd5ESCqxuEd6M3a",
"feePayer": "J3YyEVXxQNU24C3cSRm4tY1GuWAxYmpSbTCAuxSTQY1Y",
"nonceInfo": null,
"instructions": [
{
"keys": [
{
"pubkey": "J3YyEVXxQNU24C3cSRm4tY1GuWAxYmpSbTCAuxSTQY1Y",
"isSigner": true,
"isWritable": true
},
{
"pubkey": "AxrbyGTXVLx265cdf2jHmLduVU5uf3V1bNdQfAKeP1BZ",
"isSigner": false,
"isWritable": true
}
],
"programId": "11111111111111111111111111111111",
"data": [2, 0, 0, 0, 128, 150, 152, 0, 0, 0, 0, 0]
}
],
"signers": ["J3YyEVXxQNU24C3cSRm4tY1GuWAxYmpSbTCAuxSTQY1Y"]
}
import {
LAMPORTS_PER_SOL,
SystemProgram,
Transaction,
Keypair,
Connection
} from "@solana/web3.js";
const connection = new Connection("http://localhost:8899", "confirmed");
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();
// Generate sender and recipient keypairs
const sender = Keypair.generate();
const recipient = new Keypair();
// Define the amount to transfer
const transferAmount = 0.01; // 0.01 SOL
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = SystemProgram.transfer({
fromPubkey: sender.publicKey,
toPubkey: recipient.publicKey,
lamports: transferAmount * LAMPORTS_PER_SOL // Convert transferAmount to lamports
});
const transaction = new Transaction({
blockhash,
lastValidBlockHeight,
feePayer: sender.publicKey
}).add(transferInstruction);
transaction.sign(sender);
console.log(JSON.stringify(transaction, null, 2));

Click 'Run' to see output.
Program Logs in the Output are clickable links to Solana Explorer.
You must enable the "Enable Custom URL Param" setting on Solana Explorer.
If not enabled, links will default to localhost:8899 instead of the Mirror.ad RPC URL.

When you fetch a transaction using its signature after sending it to the network, you'll receive a response with the following structure.

The message field contains the following fields:

  • header: Specifies read/write and signer privileges for addresses in the accountKeys array

  • accountKeys: Array of all account addresses used in the transaction's instructions

  • recentBlockhash: Blockhash used to timestamp the transaction

  • instructions: Array of instructions to execute. Each account and programIdIndex in an instruction references the accountKeys array by index.

  • signatures: Array including signatures for all accounts required as signers by the instructions on the transaction. A signature is created by signing the transaction message using the corresponding private key for an account.

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?

Mục lục

Chỉnh sửa trang