Here I am once again, with my solana's validator infra journey, hit the idea that reframed how I think about transactions entirely,
"Runnable" is not a property of a transaction. It's a property of a transaction relative to one bank's state at one moment.
The same packet can resolve differently, or fail entirely โ against a different bank. A packet isn't universally "a transaction." It's a transaction with respect to a bank. Here's the code path that taught me why.
I'd assumed SigVerify takes packets and hands "verified transactions" downstream. But NO!
It doesn't drop bad packets โ it marks them. Invalid ones just get a discard bit flipped (set_discard(true)), the batch stays contiguous in memory and downstream stages skip the marked ones. Marking beats dropping because you avoid compacting the batch on the hottest path in the validator.
And it only checks the signature. It never deserializes bytes into a transaction. After SigVerify, what crosses into banking is still raw bytes + metadata.
So where do bytes actually become a transaction? pub(crate) fn translate_to_runtime_view. It's a three-step type-state transformation:
raw bytes
โ SanitizedTransactionView (parse + structural checks)
โ RuntimeTransaction<SanitizedTransactionView> (compute msg hash + metadata)
โ RuntimeTransaction<ResolvedTransactionView> RUNNABLE
ALT resolution, A v0 transaction doesn't list all its accounts โ it references on-chain lookup tables by index. Until you read those tables from a specific bank's state and expand them into real addresses, you don't even know the full set of accounts it touches. So you can't schedule it, and you can't execute it.
That's the deep reason SigVerify and Banking are separate stages:
โ Signature validity is universal. Verify once, early, massively parallel across cores/GPU, zero shared state.
โ Transaction resolution is stateful. It needs the leader's working bank, so it happens late, inside banking, where that bank lives.
Two completely different execution profiles โ one embarrassingly parallel, one lock-contended and bank-bound.
And the type system enforces the whole thing. SanitizedTransactionView and ResolvedTransactionView are different types, and the scheduler only accepts the resolved one.
Reading actual code >>> reading docs!