The Loader and the Verifier
The previous page, Programs as SBF Bytecode, left us with a compiled artifact: a blob of SBF (Solana Bytecode Format) instructions that is the program. But a blob on your laptop is not a program on the chain. Something has to take that blob, put it somewhere every validator can find it, and — before a single instruction executes — decide whether it is safe to run at all.
That “something” is two cooperating pieces: the loader and the verifier. Together they are the gate. This page is about how untrusted code — code written by a stranger, deployed permissionlessly, that every validator on Earth must execute identically to stay in consensus — gets vetted once, up front, so that by the time it runs, the runtime already knows it cannot escape its box or spin forever undetected. This is the piece of the answer to our throughline that says: you can run a stranger’s code at hardware speed only if you can prove, cheaply and in advance, that it can’t take the machine down.
Where does a program live?
Section titled “Where does a program live?”Start from the account model. On Solana, everything is an account: a slab of bytes with an owner, a lamport balance, and a data buffer. Your wallet is an account. A token mint is an account. And a program — the executable itself — is also just an account.
Two flags on the account make it a program rather than plain state:
executable: true— the runtime is allowed to invoke this account’s data as code.owner= a BPF loader — the program that “owns” this account is one of the on-chain loaders, not the System program or a user program.
a normal data account an executable (program) account ───────────────────── ─────────────────────────────── owner: TokenProgram owner: BPFLoaderUpgradeable executable: false executable: true lamports: 2_039_280 lamports: 1_141_440 (rent-exempt) data: [ token acct state ] data: [ SBF ELF bytes ... ] ▲ the compiled program lives here, in account data, like any other stateThe key idea, and it is a genuinely first-principles one: there is no separate “code section” of the blockchain. Code is data. The program you deploy occupies an account exactly the way a balance or a counter does — it just happens to be marked executable and owned by a loader. That uniformity is what lets the same replication, rent, and addressing machinery carry both state and logic. One mechanism, no exceptions.
The loader is a program, too
Section titled “The loader is a program, too”The thing that installs and manages these executable accounts — the loader — is itself a program that ships with the runtime (a native program). When you run solana program deploy, you are sending a transaction to a loader, asking it to write your bytecode into a new executable account it will own.
Modern Solana uses the upgradeable BPF loader, which splits a deployed program across two accounts:
Program account ProgramData account ─────────────── ─────────────────── executable: true executable: false points to ──────────────────► [ SBF bytecode ][ upgrade authority ] (a stable program id) (the actual code + who may replace it)The program account holds a stable address (the program id everyone invokes) that simply points at the current ProgramData account, which holds the real bytecode and records the upgrade authority. This indirection is what makes upgrades possible without changing the program’s public id — we return to that at the end of the page.
Why the code must be checked before it runs
Section titled “Why the code must be checked before it runs”Now the hard part. A validator is about to take bytes written by an anonymous deployer and feed them to a virtual machine, inside consensus, on every node simultaneously. What could go wrong?
Consider the failure modes of running arbitrary bytecode blindly:
- An instruction with an opcode the VM doesn’t recognize — undefined behavior, and different validators might handle it differently, which forks the chain.
- A jump to an offset outside the program — reading or executing whatever happens to be adjacent in memory: a sandbox escape.
- A malformed instruction — a multi-byte instruction that runs off the end of the code, so the “next” instruction is garbage.
- A backward jump forming an unbounded loop — a program that never returns, freezing the validator mid-block.
Any one of these, executed during block production, is catastrophic: not a failed transaction but a stalled or diverging validator. So Solana refuses to discover these problems at runtime. It proves their absence first, with a static verifier — a pass over the bytecode that runs before execution and either accepts the whole program or rejects it outright.
What the verifier statically checks
Section titled “What the verifier statically checks”The verifier walks the instruction stream once and enforces a set of structural invariants. It is not running the program; it is reading it, the way a linker reads an object file. The checks are roughly:
for each instruction in the program: ✓ opcode is in the allowed SBF instruction set ✓ the instruction is well-formed (correct length, valid registers) ✓ any jump target lands ON a real instruction boundary, IN-bounds ✓ no division by a constant zero, no writes to read-only registers ✓ control flow is bounded — no unbounded, unmeterable loop structure and: ✓ the ELF container itself parses (sections, entrypoint, relocations)Two of these deserve a second look, because they are the heart of the sandbox guarantee.
In-bounds jumps. Every jump target must be a valid instruction offset inside this program. Because there are no computed jumps into arbitrary addresses, the verifier can enumerate every place control flow can go. A program simply cannot construct an address that points outside itself. That is a huge part of “can’t escape its box” — proven by construction, not by hoping.
Bounded control flow. The verifier rejects control-flow shapes it cannot reason about. Combined with the runtime’s compute metering (the next page), this closes the halting-problem trapdoor: you cannot statically prove an arbitrary program halts, but you can insist that its control flow is well-structured enough to be metered, and then bill every instruction as it runs so an infinite loop dies when it runs out of budget rather than when it runs out of the validator’s patience.
Under the hood — the gate closes at load, not mid-block
Section titled “Under the hood — the gate closes at load, not mid-block”Put the loader and verifier together and you get a single, load-bearing property: rejected bytecode never executes.
solana program deploy ./target/deploy/prog.so │ ▼ [ loader receives the bytes ] │ ▼ [ VERIFIER: static structural checks ] │ ├── FAIL ──► transaction errors; account is NOT marked executable; │ nothing ran; the chain never saw a single opcode execute │ └── PASS ──► bytecode written to ProgramData; account marked executable │ ▼ (later) a tx invokes the program id │ ▼ [ runtime loads verified bytecode → JIT/interpret → run in sandbox ]The consequence is subtle but crucial for a global state machine: a bad binary fails at deploy or load time, which is a local, recoverable, non-consensus event — the deploy transaction just errors. It does not fail mid-execution during block production, which would be a consensus-time event that could stall or fork the network. The verifier moves the failure to the safest possible moment. That is the difference between “this program was rejected” and “this program took down the validator on slot 12,481,993.” One is a shrug; the other is an outage.
Three chains, three stances on “the halting problem”
Section titled “Three chains, three stances on “the halting problem””This is the same problem every programmable chain faces — how do you let strangers run code without letting them run forever? — and it is worth seeing Solana’s answer as one point in a design space, because the contrast is illuminating rather than incidental.
the danger: untrusted code that loops forever / escapes
Bitcoin Script FORBID loops entirely. No jumps back, no unbounded iteration in the language at all. Simplicity as safety: if you can't express a loop, you can't write an infinite one.
Ethereum / EVM ALLOW loops, PRICE every step. The VM is Turing-complete; gas meters each opcode and halts you when you run out. Safety is bought at runtime, per instruction, with a fee.
Solana / SBF VERIFY structure UP FRONT, then METER at runtime. The verifier rejects malformed/unboundable bytecode before it runs (this page); compute units bill each instruction while it runs (next page). Two gates, not one.Read down the table and the philosophies separate cleanly. Bitcoin buys safety by removing expressiveness — you cannot write the dangerous program. Ethereum buys it entirely at runtime — you can write anything, and the meter stops you. Solana does both: it uses a static pass to throw out structurally-broken or unboundable code before execution (cheap, one-time, and it protects the VM itself), and then it meters compute at runtime for the programs that pass (which handles the halting cases the static pass deliberately doesn’t try to solve). The verifier protects the machine; the meter protects the slot’s budget. Neither alone is sufficient; together they let untrusted, expressive code run at hardware speed.
If you built the mini-runtimes in the Rust playbook, you kept programs in a registry rather than loading bytecode — the verifier is precisely the piece a real chain needs that a trusted-registry toy can skip, because a toy’s programs aren’t hostile.
Upgradeable vs immutable: a power and a trust trade-off
Section titled “Upgradeable vs immutable: a power and a trust trade-off”Because a program is just data in an account, an obvious question follows: can you change it? The upgradeable loader says yes — and that “yes” is one of the most consequential trust decisions on the chain.
The ProgramData account records an upgrade authority: a single key permitted to replace the bytecode. An upgrade is exactly what it sounds like — the authority sends a new .so file, the verifier checks it, and on success the ProgramData account’s bytecode is swapped for the new version. The program id never changes, so every caller and every stored reference keeps working. The code underneath them just… changed.
upgradeable immutable ─────────── ───────── upgrade_authority: Some(devKey) upgrade_authority: None devKey can replace the bytecode NO ONE can replace the bytecode, ever → can patch bugs → what you audited is what runs, forever → MUST trust devKey not to rug → a bug is permanent; no fixesSetting the upgrade authority to None makes the program immutable: the bytecode is frozen for the life of the account, and even the original author cannot touch it. This is the trade-off in its purest form:
- Upgradeable buys you the ability to fix bugs and ship features — at the cost of a trusted key. Whoever holds the upgrade authority can, in principle, replace a safe program with a malicious one after users have deposited funds. The verifier still runs on the new bytecode, so the new code is structurally safe — but “structurally safe” is not “does what you expected.” Verification proves the code can’t escape the sandbox; it does not prove the code is honest.
- Immutable buys you certainty — the audited bytecode is the bytecode, forever — at the cost of being unable to patch a discovered vulnerability. A bug becomes a permanent property of the chain.
Serious protocols manage this deliberately: a project might keep upgradeability behind a multisig or a timelock during early development, then revoke the authority (or hand it to governance) once the code is battle-tested. The point for this page is that the loader’s flexibility is a security surface, not a convenience. The verifier guards against the machine breaking; the upgrade authority is a human trust assumption the verifier cannot check for you.
The architect’s lens
Section titled “The architect’s lens”- Why does it exist? Because a blockchain must run code written by anonymous strangers, identically, on every validator, inside consensus — and doing that blindly means one malformed or infinite program can stall or fork the entire network. The loader/verifier is the gate that makes running untrusted code survivable.
- What problem does it solve? It moves the discovery of unsafe bytecode from runtime (catastrophic, mid-block, consensus-time) to load time (local, recoverable, a mere failed transaction). It statically proves the code stays in its sandbox — valid opcodes, in-bounds jumps, well-formed, boundable control flow — before a single instruction executes.
- What are the trade-offs? Verification adds a one-time cost at deploy/load and constrains what bytecode is legal (you can’t emit anything the verifier can’t reason about). The upgradeable loader adds a trust surface: an upgrade authority can replace verified-safe code with different verified-safe code. Verification proves structure, never intent.
- When should I avoid it? You don’t avoid the verifier — it is not optional. But you do choose immutability over upgradeability once code is audited and stable, trading the ability to patch for the guarantee that what you audited is what runs forever.
- What breaks if I remove it? Everything the throughline depends on. Without the verifier, a single deployed program could escape its sandbox, read adjacent memory, or loop forever — stalling validators mid-block and forking the chain. The “single global state machine at hardware speed” stops being safe the instant it will run any bytes handed to it.
Check your understanding
Section titled “Check your understanding”- Where does a program’s compiled bytecode physically live on the chain, and which two properties of that account distinguish it from an ordinary data account?
- Name three structural properties the verifier checks statically, and for each, say what disaster it prevents if the check were skipped.
- The verifier runs at deploy/load, not on every invocation. Why is that the right place for it, both in terms of cost and in terms of when a failure is safe to occur?
- Bitcoin Script forbids loops; the EVM prices them at runtime. Describe Solana’s two-part stance and explain what the verifier handles versus what compute-unit metering handles.
- An upgradeable program passed verification when deployed. A user deposits funds, then the upgrade authority swaps in new bytecode that also passes verification but drains the account. What did verification guarantee, what did it not guarantee, and how would immutability have changed the outcome?
Show answers
- It lives in account data — the same
databuffer any account has. With the upgradeable loader the bytecode sits in a ProgramData account that the program account points to. The distinguishing properties areexecutable: true(the runtime may invoke the data as code) andownerset to a BPF loader rather than a user or the System program. - Any three: valid opcodes only (skipping it → the VM hits an undefined instruction and different validators may diverge, forking the chain); in-bounds jumps (skipping it → control flow escapes the program into adjacent memory, a sandbox escape); well-formed instructions (skipping it → a malformed multi-byte instruction runs off the end and the next “instruction” is garbage); bounded control flow (skipping it → an unmeterable/unbounded loop that the runtime can’t reason about).
- Cost: the bytecode is immutable until an explicit upgrade, so a structural property that can’t change need only be checked once and then trusted for every future call — verifying per-invocation would re-pay a fixed cost thousands of times. Safety of timing: a failure at deploy/load is a local, recoverable, non-consensus event (the transaction just errors); the same failure discovered mid-execution would be a consensus-time event that could stall or fork block production. The verifier moves failure to the safest moment.
- Solana verifies structure up front and meters at runtime. The verifier (this page) rejects malformed, out-of-bounds, or unboundable bytecode before it runs — protecting the VM/sandbox itself. Compute-unit metering (next page) bills each instruction as it executes and halts a program that exceeds its budget — handling the halting/termination cases the static pass deliberately doesn’t try to solve. Two gates: one protects the machine, one protects the slot’s budget.
- Verification guaranteed the new bytecode is structurally safe — valid opcodes, in-bounds jumps, can’t escape the sandbox. It did not guarantee the code is honest or does what users expected; verification proves structure, never intent. Immutability (upgrade authority set to
None) would have made the deployed, audited bytecode unchangeable, so the drain-swap could never have happened — at the cost of being unable to patch any bug in the original code.