Skip to content

The Instruction: Programs, Accounts, and Access Flags

The transaction anatomy page opened the envelope and showed you its parts: signatures, a message, and inside the message, a list of instructions. This page zooms all the way into a single instruction — the atom of work on Solana — and dissects its three fields. By the end you will see that an instruction is not just “a call”; it is a contract with the runtime, and the terms of that contract are precisely the information the parallel scheduler needs.

Here is the thesis, stated up front so every detail below hangs off it: an instruction declares its whole footprint before it runs. It names the one program to invoke, the exact list of accounts it will touch, and — for each account — whether it will write to it and whether that account signed. Nothing is hidden. That honesty is not a courtesy to the reader; it is the load-bearing precondition for running non-conflicting transactions in parallel, which is Solana’s entire throughput bet.

Strip away the wire format and an instruction is three things:

┌──────────────────────── one instruction ────────────────────────┐
│ │
│ program_id → which program to run (a 32-byte Pubkey) │
│ │
│ accounts → the ordered accounts it touches │
│ [ AccountMeta, AccountMeta, AccountMeta, … ] │
│ each: { pubkey, is_writable, is_signer } │
│ │
│ data → an opaque byte blob (Vec<u8>) │
│ │
└──────────────────────────────────────────────────────────────────┘

That is the entire structure. Let’s take the three fields one at a time, because each answers a different question the runtime keeps asking.

The program_id is a 32-byte Pubkey: the address of the program this instruction invokes. Recall from everything is an account that a program is just an account with executable = true, whose data is loadable SBF bytecode. So an instruction does not contain code — it points at code by address. The runtime looks up that account, confirms it is executable, and hands it the other two fields.

The book’s companion crate, solmini, keeps this exact shape. A transaction names its program by id, and the runtime resolves the id to a program to run:

pub struct Transaction {
pub program: Pubkey, // which program to invoke
pub accounts: Vec<AccountMeta>, // the accounts it touches, in order
pub data: Vec<u8>, // the opaque instruction blob
}

(solmini models one instruction per transaction for clarity; real Solana packs a list of instructions into one message, but each instruction has these same three fields.)

The accounts — what state does it touch, and how?

Section titled “The accounts — what state does it touch, and how?”

This is the field that makes Solana Solana. The accounts list is an ordered vector of AccountMeta entries. Order matters: the program receives its accounts as a positional slice — accounts[0], accounts[1], and so on — and it knows by convention which position means what (accounts[0] is the source, accounts[1] the destination, and the like). This is the “which accounts” half of the up-front footprint declaration.

But an AccountMeta is more than a pubkey. It carries two flags per account:

pub struct AccountMeta {
pub pubkey: Pubkey,
pub is_writable: bool, // will the program WRITE this account, or only read it?
pub is_signer: bool, // did the holder of this account's key SIGN the transaction?
}

Those two booleans are the whole point of this page. is_writable answers how the account is touched; is_signer answers who authorized touching it. We spend the rest of the page on them.

The data field is a raw Vec<u8> — an opaque byte blob. The runtime does not parse it or care what is inside; it hands the bytes straight to the program, which interprets them however it likes. Conventionally the first byte(s) select which handler to run within the program (a “discriminator”), and the rest are the serialized arguments. In solmini’s transfer program, data is simply the transfer amount as a little-endian u64:

// The program reads its argument straight out of the opaque blob.
let amount = u64::from_le_bytes(data[..8].try_into()?);

The blob is opaque on purpose: keeping arguments out of the account list means the runtime can reason about the footprint (the accounts) without understanding the program’s argument encoding. Meaning is the program’s business; footprint is the runtime’s.

The pubkey tells the runtime which account. The two flags are the two orthogonal assertions an instruction makes about that account. They are independent — an account can be any of the four combinations — and each is checked by a different part of the runtime.

is_signer = false is_signer = true
┌──────────────────────────┬──────────────────────────┐
writable= │ read-only, no signature │ writable, and signed: │
false │ e.g. a config/sysvar │ a read-only authority │
│ the program only reads │ proving it approved │
├──────────────────────────┼──────────────────────────┤
writable= │ writable, no signature: │ writable AND signed: │
true │ a data account the │ the classic "my wallet, │
│ program is authorized │ I signed, debit it" │
│ to change (by ownership)│ case │
└──────────────────────────┴──────────────────────────┘

The is_writable flag declares intent: this instruction will modify this account. If the flag is false, the account is read-only for the duration — the program may inspect its bytes but the runtime will reject any attempt to change them. If true, the program is permitted to mutate the account’s data or its lamports (subject to the ownership rule from everything is an account: only the account’s owner program may actually write it).

The reason this flag exists is not permission — ownership already governs who may write. The flag exists so the write is declared before execution. That single word, before, is the hinge of the whole design, and it deserves its own section.

The writable flag is the conflict information

Section titled “The writable flag is the conflict information”

Recall the conflict rule that powers parallel execution:

Two transactions conflict if they share any account and at least one of them writes it. Read–read on the same account is not a conflict.

Look at what that rule needs to evaluate: for each account a transaction touches, is it a write? That is exactly the is_writable flag, and nothing more. The scheduler never has to run a transaction to learn its writes — the instruction already told it. Here is the rule in code, from solmini, reading nothing but the flags:

pub fn conflicts(a: &Transaction, b: &Transaction) -> bool {
for ma in &a.accounts {
for mb in &b.accounts {
// shared account AND at least one side writes it → conflict
if ma.pubkey == mb.pubkey && (ma.is_writable || mb.is_writable) {
return true;
}
}
}
false
}

This is why the up-front declaration is not bureaucracy. The scheduler groups non-conflicting transactions into a batch and runs them across cores at once; two transactions that both write account A are forced into separate, ordered batches. The is_writable flag is the raw material the Sealevel scheduler consumes to decide what can run in parallel. Every time you mark an account writable, you are handing the scheduler a fact it will use to keep the network correct at speed. Mark an account read-only that two transactions share, and they run together; mark it writable, and they run in order. The flag is a throughput decision.

tx0 writes A, reads C tx1 reads A, writes B
───────────────────────── ─────────────────────────
share A: tx0 writes it ──► CONFLICT ──► different batches, run in order
tx2 writes B, reads C tx3 writes D, reads C
───────────────────────── ─────────────────────────
share only C, both read it ──► NO CONFLICT ──► same batch, run in parallel

The second flag is about authority, not layout. is_signer = true asserts: the private key behind this account’s pubkey produced a signature over this transaction. It is how an instruction proves it is allowed to act on an account that requires the account-holder’s approval — draining a wallet, closing an account, transferring an authority.

The flag is not self-certifying. It is a claim the runtime checks against the transaction’s actual signatures. Recall the transaction anatomy: a transaction carries a list of ed25519 signatures over its message bytes, and the message lists the signer accounts first. During validation the runtime verifies each signature against the corresponding pubkey. An instruction that flags an account is_signer but for which no valid signature is present is rejected before any program runs. So the flag and the signature are two halves of one mechanism:

message signers: [ alice_pubkey, ... ] (declared in the message header)
signatures: [ sig_over_message_by_alice ] (verified: ed25519(msg, alice_pubkey)?)
instruction says: AccountMeta { pubkey: alice, is_signer: true, is_writable: true }
runtime: signature present and valid for alice? ── no ──► reject transaction
│ yes
program runs, trusting accounts[i].is_signer == true means "alice approved this"

Inside the program, then, is_signer is a trustworthy fact: if the program sees an account marked as a signer, the runtime has already proven a valid signature exists for it. The program’s job is only to check that the right account signed (e.g. “the signer must equal this vault’s stored authority”), not to verify the signature itself. solmini deliberately does not model signatures — it trusts its declarations to keep the parallel-scheduling lesson uncluttered — but on real Solana this flag is where authorization lives.

The Anchor mapping: you already write these declarations

Section titled “The Anchor mapping: you already write these declarations”

If you have written an Anchor program, you have been declaring instructions in exactly this shape — Anchor just gives the fields names and generates the (de)serialization glue. Put a real Anchor handler beside the three-field model and every annotation lines up:

use anchor_lang::prelude::*;
declare_id!("Counter111111111111111111111111111111111111"); // ← the program_id
#[program] // ← the program: stateless code
pub mod counter {
use super::*;
pub fn increment(ctx: Context<Increment>, step: u64) -> Result<()> { // ← step is the data blob
let c = &mut ctx.accounts.counter; // ← accounts[i], but named
c.count = c.count.checked_add(step).unwrap();
Ok(())
}
}
#[derive(Accounts)] // ← the instruction's accounts list (the declaration)
pub struct Increment<'info> {
#[account(mut)] // ← AccountMeta { is_writable: true }
pub counter: Account<'info, Counter>, // ← a writable data account
pub authority: Signer<'info>, // ← AccountMeta { is_signer: true }
}
#[account]
pub struct Counter { pub count: u64 }

Read the annotations as the three fields:

  • declare_id!(...) is the program_id — the program’s address.
  • #[derive(Accounts)] is the accounts list — the up-front footprint declaration the scheduler reads. Writing this struct is feeding Sealevel its parallelism information. Remember that sentence.
  • #[account(mut)] sets is_writable: true; a field without it is read-only. This is the flag the conflict rule consumes.
  • Signer<'info> sets is_signer: true; Anchor additionally checks at runtime that a valid signature is present for that account, turning “I claim this signed” into “the runtime proved this signed.”
  • The step: u64 argument is the data blob — Anchor deserializes the instruction args out of the opaque bytes so you receive typed arguments instead of a &[u8].

So the hand-rolled AccountMeta { pubkey, is_writable, is_signer } and the Anchor #[account(mut)] / Signer annotations are the same declaration at two altitudes. Anchor is ergonomics over the model; the model is what the runtime actually schedules on.

Under the hood — why the flags are on the instruction, not the account

Section titled “Under the hood — why the flags are on the instruction, not the account”

It is worth asking: why do these flags live on the instruction’s AccountMeta, rather than as properties of the account itself? Because they are not properties of the account — they are properties of this transaction’s intended use of it. The same account is writable in one transaction and read-only in another; a wallet signs one transaction and is merely a payee (read-only, unsigned) in the next. The flags describe a relationship between a transaction and an account, valid only for that transaction. That is why the footprint declaration has to be re-stated per instruction: it is the runtime asking, for this specific unit of work, what will you touch and who approved it? A per-account flag could not answer that; only a per-instruction one can — and only a per-instruction declaration can be scheduled.

  • Why does it exist? An instruction is the smallest schedulable unit of work, and its three fields exist so the runtime can know a transaction’s entire footprint — which code, which accounts, which writes, which signatures — before executing it.
  • What problem does it solve? It makes conflict detection and authorization static. The scheduler reads the writable flags to decide what runs in parallel, and the runtime reads the signer flags to decide what is authorized — both without running a line of program code.
  • What are the trade-offs? The declaration must be complete and honest: every account (including those a CPI will reach transitively) has to be listed, and a wrong writable flag is a correctness or performance bug. You pay in verbosity and up-front bookkeeping for the ability to parallelize.
  • When should I avoid it? You don’t avoid it on Solana — it is the atom of every transaction. The anti-pattern to avoid is over-declaring writes: marking an account writable when you only read it needlessly serializes your transaction against every other one touching that account, throwing away parallelism for nothing.
  • What breaks if I remove it? Drop the writable flag and the scheduler can no longer tell reads from writes, so it must treat every shared account as a conflict and run everything serially — Solana’s parallelism collapses. Drop the signer flag and there is no static proof of authorization, so programs would have to reinvent signature checking by hand.
  1. Name the three fields of an instruction and say, in a few words each, what question each one answers for the runtime.
  2. An AccountMeta carries two flags. State what is_writable and is_signer each assert, and to which part of the runtime each assertion matters.
  3. Explain precisely why the is_writable flag is “the exact information the Sealevel scheduler needs.” Reference the conflict rule.
  4. The is_signer flag is a claim, not a proof. What checks it, and what has the runtime guaranteed by the time a program observes accounts[i].is_signer == true?
  5. Map each of these Anchor annotations onto the three-field instruction model: declare_id!, #[derive(Accounts)], #[account(mut)], Signer<'info>, and a step: u64 handler argument.
Show answers
  1. program_idwhat code runs? (the 32-byte address of the executable program to invoke). accountswhat state does it touch, and how? (an ordered list of AccountMeta, each with its pubkey and two flags). datawhat arguments? (an opaque byte blob the runtime passes straight to the program, which interprets it).
  2. is_writable asserts this instruction will modify this account — it matters to the scheduler, which uses it to detect conflicts and decide what runs in parallel. is_signer asserts a valid signature over the transaction exists for this account’s key — it matters to the runtime’s validation stage, which verifies it against the transaction’s signatures before any program runs.
  3. The conflict rule is: two transactions conflict if they share an account and at least one writes it (read–read is not a conflict). Evaluating it needs exactly one fact per shared account — is it a write? — which is precisely is_writable. Because every instruction declares its writable set up front, the scheduler can partition transactions into conflict-free parallel batches without executing any of them.
  4. The runtime’s signature-verification stage checks it, against the ed25519 signatures the transaction carries over its message (whose header lists the signer accounts). By the time a program sees is_signer == true, the runtime has already proven a valid signature exists for that key — so the program need only check that the correct account signed (e.g. it matches a stored authority), not verify the signature itself.
  5. declare_id! → the program_id. #[derive(Accounts)] → the instruction’s accounts list (the up-front footprint declaration). #[account(mut)]is_writable: true on that account’s AccountMeta. Signer<'info>is_signer: true, plus a runtime check that the signature is present. step: u64 → a typed argument deserialized from the opaque data blob.