Sign in

Instructions and transactions

Accounts hold the state. Instructions change it. A transaction is the envelope that carries one or more instructions to the chain, signed by whoever has the authority to make them happen. Nothing else on Solana changes state. Every balance update, every program deployment, every NFT mint is a transaction.

An instruction is a function call

The easiest way to picture an instruction is as a single function call on the blockchain. Three pieces:

The program_id says which function you want to run. It's the address of the program account that holds the code. Calling the Token Program's transfer function means setting program_id to the Token Program's address.

The accounts list is the set of accounts the program will need to do its job. The reason this list has to be supplied up front rather than discovered during execution is the parallel-execution argument from the previous lectures. The runtime has to know, before scheduling, which state the instruction will touch. Each entry in the list carries two flags: writable, meaning the program may modify this account, and signer, meaning this account's owner signed the transaction.

The data field is the raw arguments to the call, packed into bytes. The first byte usually identifies which function inside the program you want, since one program typically exposes many functions: transfer, mint, burn, and so on. The rest of the bytes are the argument values.

Anatomy of an instruction one instruction program_id which program to call e.g. Token Program address accounts the list of accounts the program will touch: - alice_usdc (writable) - bob_usdc (writable) - alice_wallet (signer) data the arguments, as bytes: [3, 100_000_000] (transfer, 100 USDC) function name argument handles argument values Think of an instruction as one function call. Program is the function, accounts are the inputs.

A transaction wraps one or more instructions

A transaction is the unit that gets signed and submitted to the network. Its job is to carry a list of instructions, plus a bit of header information that lets the network process them safely.

The header has two important pieces. The signatures field is a list of signatures, one per account that signed. Signing happens at the transaction level rather than at the instruction level, so every signer authorizes every instruction in the transaction at once. The recent_blockhash is the hash of a recent block. It proves the transaction was built recently. Validators reject transactions whose blockhash is more than about 150 blocks old, which prevents replays of the same transaction far in the future.

The instructions inside the transaction execute in order, top to bottom, and they execute atomically. Either every instruction succeeds and the chain commits all of their state changes, or one of them fails and none of the changes happen. There is no partial state. This is the same atomicity you get from a database transaction. The fee is charged either way, since the validators did the work to try.

A transaction wraps one or more instructions one transaction signatures one per signer, signed at the transaction level recent_blockhash proves the transaction is fresh (expires in ~150 blocks) instructions a list, executed in order, all-or-nothing 1. Token Program accounts: [alice_balance, bob_balance] transfer 100 USDC 2. Memo Program accounts: [] "thanks for lunch" 3. Compute Budget accounts: [] set priority fee Atomic execution: every instruction succeeds, or the whole transaction reverts. No partial outcomes. Either all three above happen, or none of them do. The fee is charged either way.

This is why batching multiple instructions in one transaction is useful. A swap on a decentralized exchange might be three instructions: approve the trade, execute the swap, refund leftover funds. Packaging them as one transaction means the trade either fully happens or fully does not. The user never ends up in a halfway state where the approval went through but the swap did not.

The access list is the access-control layer

The accounts list is doing more work than it looks like at first. Every account a transaction touches has to appear on this list, marked as read-only or writable, and marked as a signer or not. The runtime uses this list for three things, all of them before any program code runs.

First, parallel scheduling. Two transactions whose writable account sets do not overlap can run side by side on different threads. The runtime can sort the incoming traffic into batches just by reading these lists. If the lists were not declared up front, the runtime would have to actually run each transaction to find out what it touched, which would defeat the whole parallel-execution goal.

Second, signer enforcement. If an instruction expects an account to be a signer, say a transfer expecting the sender to have signed, the runtime checks that the corresponding signature is present in the transaction's signatures list. If not, the transaction fails before the program even loads. Your program code can rely on the fact that any account marked as a signer was actually signed for.

Third, owner enforcement. Before letting a program write to a writable account, the runtime checks that the account's owner field matches the program being invoked. A program cannot scribble on accounts it does not own. This is the security model from the accounts lecture, made operational on every transaction.

The cost of all this is that the client building the transaction has to know in advance which accounts the program will need and how each of them should be marked. The benefit is that every transaction arrives with its security and parallelism contract on the outside, fully readable by the runtime without executing a single line of code.

A worked example

Alice wants to send Bob 100 USDC. Here is what her transaction actually looks like.

Alice sends 100 USDC to Bob the transaction Alice signs and sends program: Token Program the function being called data: transfer(100_000_000) 100 USDC, in raw units accounts: alice_usdc [writable] balance going down bob_usdc [writable] balance going up alice_wallet [signer] authorizes the transfer Before any code runs, the runtime checks: 1. Are the listed signers present? alice_wallet is required, and it signed. 2. Are the writable accounts owned by the calling program? Both balances are Token-owned. 3. Is the recent_blockhash still valid? Yes. All three pass. The Token Program executes with these accounts as inputs. Result: alice_usdc.amount -= 100, bob_usdc.amount += 100 Two accounts changed. Nothing else on the chain was touched.

Three accounts on the list. Two of them are the actual balances, marked writable because their data is going to change. The third is Alice's wallet, marked as a signer because the Token Program will check that whoever owns alice_usdc authorized the transfer. Alice's wallet does not need to be writable. The Token Program does not modify the wallet itself, only the balance accounts.

When this transaction lands on the network, the runtime does its checks, then loads the Token Program's code from its data field and runs it. The program reads the data field of alice_usdc, subtracts 100 from the amount, writes the new value back. Does the same for bob_usdc in reverse. Returns success. The transaction commits, the new state is included in the next block, and the network keeps going.

That sequence, transaction, runtime checks, program execution, state commit, is how every single state change on Solana happens. Wallets calling transfer, decentralized exchanges executing swaps, NFT marketplaces minting collections, governance contracts tallying votes. They all reduce to this same loop.