Skip to content

Revision — Sealevel

This part asked a sharp version of the book’s throughline: how do you run a single global state machine at hardware speed without falling apart? A blockchain is, at bottom, one shared piece of state that every user mutates. The naive answer — process transactions one at a time — is obviously correct and obviously slow: it uses one core no matter how many the machine has. This part was the story of how Solana buys back that wasted parallelism without giving up the one property that makes a blockchain a blockchain, that everyone agrees on the same final state.

The answer, stripped to a sentence, is this: parallelism at hardware speed isn’t added on top of the runtime — it’s revealed by making conflicts visible before execution. Everything in Parallel Execution — Sealevel is a consequence of that one move. This page walks the whole arc back so you can hold it as a single idea.

The arc: hidden footprints force serial execution

Section titled “The arc: hidden footprints force serial execution”

Start with why the problem exists at all. In Why the EVM Runs Serially we saw that Ethereum’s execution model hides what each transaction touches. A contract holds its own storage, and a call can reach into any other contract, which can reach into another. Until you actually run a transaction, you don’t know which slots of state it will read or write. Its footprint — the set of state it touches — is discovered during execution, not before it.

That single fact is why the EVM serializes. If you cannot know two transactions’ footprints in advance, you cannot know whether they are independent, and if you cannot know that, the only safe order is one-at-a-time. Serial execution there is not a lazy choice; it is forced by hidden footprints. The EVM runs on one core because it has no way to prove that using two would be safe.

Solana’s design is the mirror image. In Declared Footprints: The Account-Locks Design we saw the trade it makes: every transaction must declare, up front, the full list of accounts it will read and write, and whether each is read-only or writable. State does not hide inside programs — programs are stateless code, and all mutable state lives in named, external accounts (the account model we built in the crate’s account.rs). Because the state is external and named, a transaction can name it before running. The footprint stops being a runtime discovery and becomes a static declaration.

EVM Solana
─── ──────
footprint discovered DURING run footprint DECLARED before run
→ can't prove independence → runtime sees independence
→ must serialize → can parallelize the disjoint ones

The declaration is the whole hinge. Once the runtime can see which transactions are independent, serial execution stops being mandatory. Parallelism was always latent in the workload — most transactions in a block touch completely different accounts — but the EVM couldn’t see it. Solana didn’t invent the parallelism; it made the workload declare enough that the parallelism became visible.

One rule at three altitudes: shared XOR mutable

Section titled “One rule at three altitudes: shared XOR mutable”

The rule that decides whether two transactions can run together is not new, and that was the quiet lesson of The Conflict Rule: Shared XOR Mutable for Accounts. It is the same rule you already met from Rust’s borrow checker, lifted to a new object. Say it once and watch it repeat:

You may have any number of shared readers, XOR exactly one exclusive writer — never both at once.

That rule shows up at three altitudes in this book, and it is literally the same rule each time:

  • The borrow checker (compile time). For a variable, Rust allows many &T or one &mut T, never a mix. The object is a variable; the enforcer is the compiler; the failure it prevents is a data race in your own code.
  • RwLock (run time, in-process). For a value shared across threads, a read-write lock hands out many read guards or one write guard. Same shape, now dynamic: the object is a value, the enforcer is the lock, the failure it prevents is two threads racing on shared memory.
  • The transaction scheduler (cluster-wide). For an account, the runtime lets many transactions read it, or one transaction write it, in a given batch. The object is an account, the enforcer is the scheduler, the failure it prevents is two transactions racing on shared global state.

Two transactions conflict exactly when they share an account and at least one of them writes it. Read–read on the same account is not a conflict — a thousand transactions can read the same config account in parallel, because reading cannot change what another reader sees. In the companion crate the rule is four lines, and they are worth memorizing because they are the entire safety argument:

pub fn conflicts(a: &Transaction, b: &Transaction) -> bool {
for ma in &a.accounts {
for mb in &b.accounts {
if ma.key == mb.key && (ma.is_writable || mb.is_writable) {
return true;
}
}
}
false
}

The elegance is that once you have honest declared footprints, conflicts is a pure function of two declarations — you never have to run a transaction to know whether it clashes with another. That is the payoff the EVM can never have.

The machinery: scheduling and parallel commit

Section titled “The machinery: scheduling and parallel commit”

Knowing which transactions conflict is not yet a runtime. Two more steps turn the conflict rule into throughput, and both exist to guarantee one thing: the parallel result must be byte-for-byte identical to running the same transactions in submission order. Parallelism is only allowed to be faster, never to change the answer.

The first step, from Scheduling Into Conflict-Free Batches, is layering. The scheduler assigns each transaction to the earliest batch that sits strictly after every earlier transaction it conflicts with:

batch(i) = 1 + max{ batch(j) : j < i and conflicts(i, j) } (0 if none)

This one line buys two properties simultaneously. Within a batch, nothing conflicts — if two transactions in the same batch clashed, the later one would have been pushed up a level. And conflicting transactions keep their submission order — a later writer always lands in a strictly later batch than the writers before it, so it sees their committed effects. Independent transactions collapse into one wide batch; a chain of transactions all hammering one account degrades gracefully into one-per-batch, i.e. back to serial, exactly for the accounts that demand it.

txs (submission order): t0 t1 t2 t3 t4
accounts they write: A B A C B
batch 0 (parallel): t0[A] t1[B] t3[C] ← all disjoint, run together
batch 1 (parallel): t2[A] t4[B] ← each waited on an earlier writer

The second step, from Executing a Batch in Parallel, is what makes the parallel run safe rather than merely scheduled. Each transaction runs against its own private working set — an owned clone of just the accounts it declared. It mutates that clone freely, and the runtime commits its writable accounts back into the shared database only if it succeeded. Because a batch is conflict-free, every writable account in the batch is claimed by exactly one transaction, so no two threads ever reach the same writable account and the commit never clobbers. A failed transaction is trivial to roll back: its private clone is simply discarded.

This is where the borrow-checker analogy stops being an analogy and becomes the actual implementation. In the crate, rayon hands each transaction a disjoint &mut Vec<Account>, and Rust’s own type system refuses to compile any version where two threads could alias the same writable account:

let batch_results: Vec<Result<()>> = batch
.par_iter()
.zip(work.par_iter_mut()) // disjoint &mut per transaction
.map(|(&i, accts)| Self::run_tx(programs, compute, &txs[i], accts))
.collect();

The data race that “just run these transactions on threads” invites in C is, here, unrepresentable — you cannot write the bug down, because the batch is conflict-free and the working sets are private. That is the strongest form of correctness: not “we tested for races” but “a race cannot be expressed.” The integration tests close the loop by asserting the parallel final state equals the sequential final state.

The ceiling: hot accounts and local fee markets

Section titled “The ceiling: hot accounts and local fee markets”

Parallelism is only as wide as the workload allows, and The Hot-Account Ceiling: Amdahl and Local Fee Markets was the honest accounting of its limit. The scheduler can only run transactions in parallel if they touch different writable accounts. Every transaction that writes the same account must serialize against the others — that is not a Solana flaw, it is the conflict rule doing its job. If a popular NFT mint or a single AMM pool is written by a thousand transactions in a block, those thousand transactions form a serial chain no matter how many cores the validator has.

This is Amdahl’s Law wearing an account model: the speedup from parallelism is capped by the fraction of work that must run serially, and the hot account is that serial fraction. The system’s throughput on a given account is bounded by that one account, independent of everything else happening in the block.

The consequence is the design’s most-underappreciated feature: fee markets are local. Because contention is measured per writable account, congestion on one hot account raises the price of writing that account without taxing every unrelated transaction in the block. A trading frenzy on one pool does not make your unrelated transfer more expensive, the way a single global gas auction would. The hot-account ceiling and local fee markets are the same fact seen from two sides: the runtime can only serialize per-account, so it can only price per-account too.

Put the arc back together and the shape is clean. The EVM serializes because footprints are hidden, so independence can never be proven before execution. Solana forces every transaction to declare its footprint, which makes independence a static, pre-execution fact. The rule that turns declarations into a schedule is shared XOR mutable — the borrow checker’s rule, lifted from variables to accounts. The scheduler layers transactions into conflict-free batches, and parallel execution against private working sets makes the parallel result provably identical to the serial one, with the data race left literally unrepresentable. And the hot-account ceiling is the honest limit: the runtime can only parallelize across distinct writable accounts, which is exactly why its fee markets are local.

The single idea worth carrying out of this part is the one the throughline has been circling all along. You do not make a global state machine fast by bolting threads onto a serial engine and hoping. You make it fast by changing what the runtime is allowed to know before it runs — by moving the footprint from a runtime secret to a declared fact. Once conflicts are visible up front, the parallelism was there the whole time; Sealevel just stops hiding it. Hardware speed, in the end, is not something you add to the state machine. It is something you reveal by making conflict legible before a single instruction executes.