Skip to content

Declared Footprints: The Account-Locks Design

The previous page, Why the EVM Runs Serially, ended on a diagnosis: the EVM cannot run transactions in parallel because it cannot know, before executing a transaction, which storage that transaction will touch. State is hidden inside contracts, reachable only by running the code, so the runtime is forced to run everything one at a time to be safe. The serialization is not a lazy implementation choice — it is forced by the data model.

This page is where Solana pays a different price to buy a different property. The design move is almost insultingly simple: make every transaction declare, up front, the complete set of accounts it will touch, and mark each one read-only or writable. That single requirement is the raw material the whole Sealevel scheduler consumes. Once a transaction’s footprint is knowable before it runs, the runtime can look at two transactions and answer “can these run at the same time?” without executing either. This page is about that declaration: what it looks like in code, why the account model makes it possible, and the one non-negotiable rule that keeps it honest.

Strip a Solana transaction down to what the scheduler cares about and you get three fields. Here they are in the book’s companion runtime, rust/solmini/src/runtime.rs:

/// A transaction: which program to invoke, the accounts it touches (in the
/// order the program expects them), and the opaque instruction `data`.
pub struct Transaction {
pub program: Pubkey,
pub accounts: Vec<AccountMeta>,
pub data: Vec<u8>,
}
/// One account reference inside a transaction, with its access mode declared.
pub struct AccountMeta {
pub key: Pubkey,
pub is_writable: bool,
}

Read accounts: Vec<AccountMeta> slowly, because it is the whole page. A transaction does not discover its accounts as it runs. It carries them — a list, fixed before execution, of every account the program is allowed to see, each tagged with whether the transaction intends to write it (is_writable) or only read it. That list is the declared footprint. The program field says which stateless program to invoke; the data blob is the instruction arguments. But the accounts are the part the scheduler reads.

Transaction {
program: TransferProgram ← which code to run (holds no state)
accounts: [ {key: alice, writable} ← footprint: exactly these accounts,
{key: bob, writable} ] each with its access mode
data: [ amount = 5 ] ← instruction arguments
}

Note what is not here: the transaction never names an account it will touch “later” or “if a branch is taken.” There is no later. If the program tries to reach an account that is not in this list, the transaction fails. The footprint is complete by construction — and that completeness is precisely what the EVM’s model cannot offer.

The two constructors make the footprint concrete

Section titled “The two constructors make the footprint concrete”

The companion runtime’s helpers show the footprint being built literally, one AccountMeta at a time:

/// A transfer of `amount` lamports from `from` to `to`. Both endpoints are
/// writable (both balances change), so two transfers sharing an endpoint conflict.
pub fn transfer(from: Pubkey, to: Pubkey, amount: u64) -> Self {
Transaction::new(
TransferProgram::id(),
vec![AccountMeta::writable(from), AccountMeta::writable(to)],
amount.to_le_bytes().to_vec(),
)
}
/// Increment `counter` by `step`.
pub fn increment(counter: Pubkey, step: u64) -> Self {
Transaction::new(
CounterProgram::id(),
vec![AccountMeta::writable(counter)],
step.to_le_bytes().to_vec(),
)
}

A transfer declares two writable accounts — both balances change. An increment declares one. Neither declaration required running the program to figure out; the caller who builds the transaction knows the footprint, because on Solana the accounts a program touches are always passed in, never reached out for.

Why stateless programs make the footprint declarable

Section titled “Why stateless programs make the footprint declarable”

Here is the crux, and it is the reason the account model earlier in this book was worth the pages it took. On Solana:

  • Programs are stateless. A program is pure code; it owns no storage of its own. (See Stateless Programs Act on Accounts.)
  • State lives in external, named accounts. Every mutable byte lives in an account — a balance, a data buffer, and an owner — addressed by a public key.

Put those together and the consequence is unavoidable: the only way a program can touch state is through accounts the transaction hands it. A program cannot reach into a global map and pull out storage keyed by some address it computed mid-execution, because there is no such map inside the program — the state is outside, in accounts the caller must supply. So the set of accounts a transaction can affect is knowable before the program runs, for the simple reason that it is literally the argument list.

Contrast this with the EVM directly, the way Contrast With Ethereum lays it out:

Ethereum Solana
──────── ──────
contract = code + its own storage program = code only (stateless)
SLOAD/SSTORE reach hidden slots every account passed IN as an argument
footprint discovered by RUNNING footprint DECLARED before running

An EVM contract holds its own storage, and a call can SSTORE to any slot it computes — including slots in other contracts it decides to call, decided at runtime by the data. There is no honest way to list that set in advance without running the code. Solana forecloses that entire class of surprise by moving state out of the program and forcing it in through the front door. The stateless-program decision from the Accounts part was not a stylistic preference. It was the enabling precondition for everything on this page.

is_writable is the access mode — the same flag as #[account(mut)]

Section titled “is_writable is the access mode — the same flag as #[account(mut)]”

The footprint is not just which accounts; it is how each is accessed. That is the entire job of the is_writable boolean on AccountMeta. Two access modes, and only two:

  • is_writable: falseread-only. The transaction may read this account but promises not to change it.
  • is_writable: truewritable. The transaction may modify this account.

Why does the scheduler care about the difference? Because two transactions that only read the same account do not interfere — reading is safe to do concurrently. Two transactions where at least one writes a shared account do interfere. The is_writable flag is what lets the scheduler tell those cases apart without running anything, and the next page, The Conflict Rule, is built entirely on it.

If you have written an Anchor program, you have already declared this flag by hand. The Anchor account constraint #[account(mut)] is exactly is_writable: true:

#[derive(Accounts)]
pub struct Transfer<'info> {
#[account(mut)] // is_writable: true — Anchor will mark this writable
pub from: Account<'info, Wallet>,
#[account(mut)] // is_writable: true
pub to: Account<'info, Wallet>,
pub config: Account<'info, Config>, // no `mut` → is_writable: false, read-only
}

When you leave mut off, Anchor marks that account read-only in the transaction’s AccountMeta. When you add it, Anchor marks it writable. Every #[account(mut)] you have ever written was you filling in a bit of the footprint the scheduler will later read. Anchor and the wire format even split the writable and read-only accounts into ordered groups so the runtime can find them quickly; see The Instruction and Account Flags for how that ordering is encoded on the wire.

Honesty: a transaction that lies about its footprint is unschedulable

Section titled “Honesty: a transaction that lies about its footprint is unschedulable”

The entire scheme rests on one assumption: the declared footprint is the truth. The scheduler decides which transactions may run together purely from their declarations. If a transaction could declare “I touch only account A” and then, mid-execution, reach out and write account B, the scheduler’s guarantee would be a fiction — it might have run that transaction in parallel with another writer of B, and now you have the exact data race the model promised to prevent.

So the declaration cannot be advisory. It must be enforced. The companion runtime enforces it structurally: the program is only ever handed the accounts the transaction named, so it physically cannot reach an undeclared one. From runtime.rs, the working set a transaction runs against is gathered from its declared list and nothing else:

/// Pull a transaction's declared accounts out of the DB as an owned working set.
fn working_set(&self, tx: &Transaction) -> Vec<Account> {
tx.accounts
.iter()
.map(|m| self.accounts.get(&m.key).cloned().unwrap_or_default())
.collect()
}

The program’s process(accounts, data) receives this slice — only the declared accounts. There is no back channel to the global account map. An undeclared account is not merely forbidden; it is unreachable.

Real Solana reaches the same guarantee by enforcement rather than by structure: a program runs inside the runtime, and any attempt to touch an account not present in the transaction’s account list — or to write an account marked read-only — fails the transaction. You cannot smuggle in an account. This is not a politeness convention; it is what keeps the schedule sound.

declared footprint ─────► scheduler decides parallelism
▲ │
│ MUST be the whole truth ▼
runtime FAILS any tx that transactions run in parallel
touches an undeclared account ONLY because the declaration is trusted

The lesson lands as a slogan: a transaction that lies about its footprint is unschedulable. If the declaration were not enforceable, the scheduler could not trust it, and if it could not trust it, it would have to run everything serially — right back to the EVM. Enforced honesty is what makes the declaration load-bearing rather than decorative.

The declaration makes conflicts visible before execution

Section titled “The declaration makes conflicts visible before execution”

Step back to the throughline: how do you build a single global state machine that runs at hardware speed without falling apart? Running at hardware speed means running on many cores at once. Not falling apart means never letting two cores clobber the same state. Those two goals fight — unless you can tell, cheaply and in advance, which transactions are independent.

The declared footprint is exactly that “in advance.” Because every transaction lists its accounts and their access modes before it runs, the runtime can look at any two transactions and see whether they share a writable account — whether they conflict — as pure bookkeeping over two lists, no execution required. Conflicts become visible before execution. That is the enabling move for the entire model. Everything downstream is a consequence:

declared footprints (this page)
│ makes conflicts computable up front
the conflict rule /sealevel/the-conflict-rule/ — shared XOR mutable, over accounts
│ defines "can these two run together?"
scheduling into batches /sealevel/scheduling-into-batches/ — group the non-conflicting ones
executing a batch in parallel /sealevel/executing-a-batch-in-parallel/ — run them across cores

Parallelism, seen this way, is not something the runtime adds. It is something the declaration reveals: the independent work was parallel all along, and declaring footprints is what lets the scheduler see it. The EVM cannot make that move because it cannot see the footprint until it is too late. Solana can, because it demanded the footprint at the door.

  • Why does it exist? Because parallel execution needs to know which transactions are independent before running them, and the only way to know that cheaply is to make each transaction declare the accounts it touches and how — read-only or writable — up front.
  • What problem does it solve? The EVM’s fatal constraint: a contract holds its own hidden storage, so the runtime cannot know a transaction’s footprint without executing it, and therefore must serialize everything. Declared footprints replace “discover by running” with “declare before running.”
  • What are the trade-offs? Transactions get bigger and are capped in how many accounts they can name; developers must correctly declare every account and its access mode, and a wrong declaration is a hard failure. You pay bytes and discipline at submission time to buy safe parallelism at execution time.
  • When should I avoid it? You don’t get to opt out on Solana — but the model is the wrong fit for workloads that are genuinely dominated by one hot, contended account, where nothing can be declared independent and the declaration buys you no parallelism (the ceiling the hot-account page confronts).
  • What breaks if I remove it? Everything downstream. Without an honest, up-front footprint the scheduler has nothing to reason over, the conflict rule cannot be evaluated before execution, batches cannot be formed — and the runtime collapses back to running one transaction at a time, exactly like the EVM.
  1. Name the three fields of Transaction in the companion runtime and say which one the scheduler actually reads to decide parallelism, and why.
  2. Explain why stateless programs plus external accounts make a transaction’s footprint declarable up front, and why the EVM’s contract-holds-storage model forecloses the same move.
  3. What does is_writable on an AccountMeta mean, and what Anchor account constraint is it exactly equal to? Why does the scheduler need this flag and not just the list of keys?
  4. “A transaction that lies about its footprint is unschedulable.” Explain what would go wrong if the declaration were merely advisory, and how real Solana (and the companion runtime) prevent it.
  5. The page frames the declaration as “making conflicts visible before execution.” Explain why that phrase is the enabling move for the entire Sealevel model, and what the runtime would be forced to do without it.
Show answers
  1. program (which stateless program to invoke), accounts: Vec<AccountMeta> (the declared footprint — which accounts, each tagged read-only or writable), and data (the opaque instruction arguments). The scheduler reads accounts, because that list is the complete, up-front footprint; the program and data determine what happens, but only the account list determines which transactions can run together.
  2. On Solana a program owns no storage — all state lives in external, named accounts that must be passed in as the transaction’s argument list. So the set of accounts a program can affect is literally its arguments, knowable before it runs. An EVM contract holds its own storage and can SSTORE to slots it computes at runtime (including in other contracts it calls), so there is no honest way to list that set in advance without executing the code.
  3. is_writable: true means the transaction may modify the account; false means read-only. It is exactly Anchor’s #[account(mut)] (present → writable, absent → read-only). The scheduler needs it because two transactions that only read the same account do not interfere, while two where at least one writes a shared account do — the flag is what distinguishes those cases without running anything.
  4. If the declaration were advisory, a transaction could declare it touches only A but then write B at runtime; the scheduler, trusting the declaration, might have run it in parallel with another writer of B, producing the exact data race the model was meant to prevent. Real Solana enforces honesty by failing any transaction that touches an undeclared account or writes a read-only one; the companion runtime enforces it structurally by only ever handing the program its declared accounts, making an undeclared account physically unreachable.
  5. Because every transaction declares its accounts and access modes before execution, the runtime can compute whether any two transactions conflict (share a writable account) as pure bookkeeping over two lists — no execution needed. That up-front visibility is the raw material the conflict rule, batch scheduler, and parallel executor all consume. Without it the runtime could not know which transactions are independent until it ran them, and would be forced to serialize everything — collapsing straight back to the EVM’s model.