The Conflict Rule: Shared XOR Mutable for Accounts
The previous page, Declared Footprints, gave the runtime the one thing the EVM never has: before a transaction runs, we already know every account it will read and every account it will write. That declaration is the raw material. This page turns it into a decision — the single yes/no question the whole Sealevel scheduler is built on: can these two transactions run at the same time?
The answer is one sentence, and it is worth memorizing before we write a line of code: two transactions are safe to run in parallel exactly when they don’t share a writable account. Everything else on this page — the code, the diagrams, the three-altitude analogy — is just making that sentence precise and earning the right to trust it. It is the load-bearing rule for the book’s throughline: how do you run a single global state machine at hardware speed without falling apart? You keep the pieces that can’t interfere apart, and this rule is how you know which pieces those are.
The conflict rule, stated precisely
Section titled “The conflict rule, stated precisely”Here is the rule, with no wiggle room:
Two transactions conflict if and only if they share at least one account and at least one of the two writes that account. If the only accounts they share are read by both, they do not conflict.
Unfolding the three cases for a single shared account makes the asymmetry obvious:
tx A on X tx B on X conflict? why ───────── ───────── ───────── ─────────────────────────────────────── read read NO two readers never disturb each other read write YES B changes X out from under A's read write read YES A changes X out from under B's read write write YES both change X; whose value wins is a raceThree of the four combinations conflict. Only read–read is free. That is not a coincidence or a tuning choice — it is the deepest fact about shared state, and the rest of the page is about why.
Why read-only sharing is always safe
Section titled “Why read-only sharing is always safe”Reason from what a reader can actually observe. During a batch, no writable account it shares with another transaction is being written — because if one were, the rule would have flagged a conflict and the two would never have been placed in the same batch. So every account a transaction reads is, for the duration of that batch, frozen. A frozen value is a consistent value: it is exactly the state left by whatever ran before this batch. Any number of transactions can read that same frozen account simultaneously and every one of them sees the identical, coherent snapshot.
The moment even one writer is added, that guarantee collapses. Now a reader might catch the account before the write, another reader might catch it after, and a third might catch it mid-update (a torn read). The readers no longer agree on what the account says — they observe an inconsistent state — and the result depends on thread timing, which is the definition of a data race. So the rule isn’t “writers are dangerous”; it’s “a writer plus anyone else on the same account is dangerous.” Sharing is safe up to, and only up to, the point where mutation enters.
One rule at three altitudes
Section titled “One rule at three altitudes”If that “many readers, but a writer needs exclusivity” shape feels familiar, it should. It is the same rule you have already met twice, at smaller scales.
scope "shared XOR mutable" says… enforced by ───────────────────── ────────────────────────────────────── ─────────────── a variable in a function any number of &T XOR one &mut T the borrow checker one shared data structure many read-locks XOR one write-lock RwLock (at runtime) a batch of transactions many readers of X XOR one writer of X the Sealevel scheduler- In a single function, Rust’s borrow checker lets you take any number of shared
references
&Tto a value, or exactly one exclusive&mut T, never both at once. A reader and a writer of the same variable is a compile error. - In one shared data structure, an
RwLock<T>hands out many read guards or one write guard. Readers coexist; a writer waits to have the structure to itself. Same rule, now enforced at runtime across threads instead of at compile time within a function. - Across a whole batch of transactions, the Sealevel scheduler lets many
transactions read account
Xor lets exactly one write it in a given batch. Same rule again, lifted from a variable to a globally-addressed account.
One idea, three altitudes: the compiler within a function, a lock within a structure, the scheduler across the network’s state. Once you see it is a single principle, the conflict check almost writes itself — because it is just this rule spelled out for account lists.
Implementing conflicts(a, b)
Section titled “Implementing conflicts(a, b)”Recall from Declared Footprints that a transaction
carries its footprint as a list of AccountMeta — each a key plus an is_writable
flag. The companion crate, solmini, defines exactly that:
/// One account reference inside a transaction, with its access mode declared.pub struct AccountMeta { pub key: Pubkey, pub is_writable: bool,}
pub struct Transaction { pub program: Pubkey, pub accounts: Vec<AccountMeta>, pub data: Vec<u8>,}The conflict check is a direct transcription of the rule. Compare each account of a
against each account of b; if the keys match and either side writes it, they
conflict:
/// Do two transactions conflict? They do when they share any account and at least/// one of them writes it (read–read on the same account is *not* a conflict).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; // shared account, at least one writer → conflict } } } false}Read the predicate on the inner line as the rule verbatim: ma.key == mb.key is “they
share this account”, and (ma.is_writable || mb.is_writable) is “and at least one of
them writes it.” The || is the whole asymmetry — it is true for write–read,
read–write, and write–write, and false only when both flags are clear, which is the
read–read case we proved safe. There is no branch that special-cases who owns the
account or which program runs; the footprint is all the scheduler needs.
solmini’s own tests pin down the read–read exemption directly:
#[test]fn conflict_rule_ignores_read_read() { let shared = Pubkey::from_seed("config"); let a = Transaction::new( TransferProgram::id(), vec![AccountMeta::readonly(shared), AccountMeta::writable(Pubkey::from_seed("a"))], vec![], ); let b = Transaction::new( TransferProgram::id(), vec![AccountMeta::readonly(shared), AccountMeta::writable(Pubkey::from_seed("b"))], vec![], ); // Both only READ `shared`; their writable accounts differ → no conflict. assert!(!conflicts(&a, &b));}Two transactions both read a shared config account and each write a different
private account. They share shared, but only for reading, so conflicts returns
false and the scheduler is free to run them together. Flip either readonly(shared)
to writable(shared) and the same test would report a conflict — that one flag is the
entire difference between “run in parallel” and “must serialize.”
A concrete pair
Section titled “A concrete pair”Make it concrete with transfers. A transfer touches two writable accounts (both
balances change), so Transaction::transfer(from, to, amt) declares from and to as
writable:
t0: transfer Alice → Bob writes {Alice, Bob} t1: transfer Carol → Dave writes {Carol, Dave} t2: transfer Alice → Erin writes {Alice, Erin}
conflicts(t0, t1)? {Alice,Bob} ∩ {Carol,Dave} = ∅ → NO conflicts(t0, t2)? share Alice, both WRITE it → YES conflicts(t1, t2)? {Carol,Dave} ∩ {Alice,Erin} = ∅ → NOt0 and t1 are disjoint and can run side by side. t2 shares Alice with t0 and
both write her balance, so they conflict — running them simultaneously would race on
Alice’s lamports, and one debit could clobber the other. The rule caught it from the
declarations alone, before either transaction executed a single instruction. Turning
this pairwise verdict into an actual parallel schedule is the job of the next page,
Scheduling Into Conflict-Free Batches.
Under the hood — the O(n²) scan is a teaching simplification
Section titled “Under the hood — the O(n²) scan is a teaching simplification”conflicts is a doubly-nested loop, and the scheduler that uses it will call it across
pairs of transactions — so as written, checking a block of n transactions is quadratic
in n (and each conflicts call is itself quadratic in the account-list lengths). That
is fine for reading the idea off the page and for solmini’s test-sized blocks. It would
not survive a mainnet block of thousands of transactions.
Real Solana computes the same verdict without ever comparing transactions pairwise. It keeps a table of per-account read/write locks. When it considers a transaction, it tries to take a read lock on each account the transaction reads and a write lock on each account it writes:
want to schedule tx with footprint {read: X, write: Y} ├─ read-lock X → OK if X has no WRITE lock held (many readers allowed) └─ write-lock Y → OK if Y has NO lock held at all (writer needs exclusivity) all locks acquired → tx joins the batch; else it waitsA read lock coexists with other read locks but not with a write lock; a write lock
demands the account be otherwise unlocked. That is exactly the conflict rule — many
readers XOR one writer, per account — but resolved by a hash-map lookup per declared
account instead of by comparing every transaction against every other. The O(n²) scan
and the lock table produce identical conflict-free batches; only the bookkeeping differs.
solmini uses the scan because it makes the rule legible; production uses the locks
because the rule has to hold at line rate.
The architect’s lens
Section titled “The architect’s lens”The conflict rule is the small idea the entire parallel runtime is balanced on, so it is worth looking at through the architect’s five questions.
- Why does it exist? Because parallelism is only safe when the things running at once cannot interfere, and the rule is the precise, cheap test for “cannot interfere” — computed from declared footprints, before any code runs.
- What problem does it solve? It converts the vague fear “these transactions might race” into a mechanical yes/no over account lists, so the scheduler can decide what to parallelize deterministically instead of guessing or locking pessimistically.
- What are the trade-offs? It is conservative: it flags any shared writable account as a conflict even when the two writes would happen to commute, so it can serialize work that is technically independent. It buys correctness and simplicity by giving up a little theoretical parallelism.
- When should I avoid it? Never, within Sealevel — but be aware the rule’s verdict depends entirely on honest footprints. If footprints were allowed to be wrong (a transaction touching an undeclared account), the rule would green-light a real race. The runtime prevents this by failing any program that touches an undeclared account.
- What breaks if I remove it? Everything downstream. Without a conflict definition there is no way to form conflict-free batches, so scheduling and parallel execution have no ground to stand on — you are back to running serially, like the EVM.
Check your understanding
Section titled “Check your understanding”- State the conflict rule exactly. Which of the four combinations of two transactions touching the same account do not conflict, and why is that the only safe one?
- Read the predicate
ma.key == mb.key && (ma.is_writable || mb.is_writable)inconflicts. Which clause encodes “they share an account” and which encodes “at least one writes it,” and which case makes the whole expressionfalse? - Explain why many transactions reading the same account simultaneously is safe, in terms of what each reader observes. What specifically breaks the moment one writer is added?
- The conflict rule is called “the same idea at three altitudes.” Name the three scopes and the mechanism that enforces the rule at each.
solminiuses anO(n²)pairwise scan, but real Solana uses per-account read/write locks. Why is the lock table not a different rule — what does it compute, and why is it faster?
Show answers
- Two transactions conflict if and only if they share at least one account and at least one of them writes it. Only read–read on the shared account does not conflict, because two readers never change the value, so neither can observe the account in an inconsistent state; the other three combinations (read–write, write–read, write–write) each involve a writer changing an account someone else touches.
ma.key == mb.keyencodes “they share this account”;(ma.is_writable || mb.is_writable)encodes “and at least one writes it.” The whole expression isfalseonly when the keys match but bothis_writableflags are clear — the read–read case — which is exactly the one safe combination.- During a batch no shared account is being written (or the rule would have flagged a conflict), so every account a reader touches is frozen at a single consistent value; any number of readers see the identical snapshot. Adding one writer means readers can catch the account before, after, or mid-update (a torn read), so they no longer agree on its value and the outcome depends on thread timing — a data race.
- (a) A variable in a function — the borrow checker (
&Tvs&mut T) at compile time; (b) one shared data structure — anRwLock(read guards vs write guard) at runtime; (c) a batch of transactions over accounts — the Sealevel scheduler (many readers vs one writer per account). Same “shared XOR mutable” rule, three scopes. - It computes the identical verdict — many readers XOR one writer, per account — just
with different bookkeeping. Instead of comparing every transaction against every other
(
O(n²)), each transaction tries to take a read lock on each account it reads and a write lock on each it writes; a hash-map lookup per declared account replaces the pairwise scan, so the same conflict-free batches emerge far faster.