ADR-0019: The program set is part of the state

A Solieum chain executes the programs an operator names at startup: The node's own help already states the consequence: "The set is part of the state…

Status: accepted (steps 1 to 4 built (see the end)). Dated 2026-09-10.

Status: accepted, 2026-09-10; steps 1 to 4 built 2026-09-13 (see the end). Step 5 remains a project.

The defect

A Solieum chain executes the programs an operator names at startup:

The node's own help already states the consequence: "The set is part of the state transition: a datadir replays only under the same set." That is correct and it is not enough, because nothing in what the chain publishes says what the set was.

Checked rather than assumed, on 2026-09-10:

  • getAccountInfo on a registered program id returns nothing. The program is not an account. There is no program-data account, no deploy transaction, no bytecode on the chain.
  • genesis.json records the L1 program ids — settlement, DA, inbox, portal — and no L2 program set. The genesis hash therefore does not cover it.
  • Nothing writes the set into state, so no state root covers it either.

So an independent verifier re-deriving a chain from its published bytes cannot learn which programs were in play. It must be told, out of band, by the operator whose work it is checking. That is the one relationship the whole design exists to avoid.

Why this matters more than it looks

The beta chain registers no programs, so this bites nothing today. It bites the first chain that registers one, and it bites in three ways.

Re-derivation stops being self-contained. solieum-node verify rebuilds every block from Solana's published bytes and compares roots. Against a chain with programs, the same bytes produce different roots under a different set, and the verifier has no way to detect that it was given the wrong one. It would report a divergence it cannot explain, or agree for the wrong reason.

A fraud proof cannot name what it is disputing. Steps through registered programs are already opaque to the dispute game, which is a known limit. But an opaque step at least says this step used a program the verifier cannot execute. It does not say which, because the chain does not know.

The explorer can only publish a claim. This is what prompted the question. An EVM explorer shows verified source because the bytecode is on chain: recompile, compare, display. Here there is nothing on chain to compare against, so the strongest honest thing an explorer can publish is the node's own statement about a file on the operator's disk. That is what it now publishes, with the caveat attached (getPrograms carries bytes and sha256, and the programs page says plainly that these are not a proof).

What is proposed

Commit the set to chain identity. Genesis gains a programs array of { id, sha256 }, covered by the genesis hash exactly as the L1 ids already are. A node started with a set that does not match its genesis refuses to start, in the same way and for the same reason it refuses a genesis whose L1 ids disagree with the cluster.

That much is small, and it turns "the operator told me" into "the chain says so". It costs a chain restart to change the set, which is correct: changing the set is changing the state transition.

Then, separately and later, a deploy path. Programs as on-chain accounts written by a loader instruction, as on Solana. That is a much larger change and it should not be bundled: it introduces program accounts into the state tree, makes the program set mutable within a chain's life, and interacts with the challenge window in ways nothing here has thought through. The commitment above is worth having on its own and does not depend on it.

What this does NOT fix

Committing the hash makes the set checkable. It does not make a program provable. Executing arbitrary compiled code inside a one-step verifier is the SBF interpreter class that notes/readiness-2026-09-03.md lists as the remaining protocol gap, and this ADR does not touch it. After this change a verifier can say "the chain committed to this program and this is the program that ran". It still cannot say "and this instruction's result was correct".

Nor does it make published source trustworthy. A hash plus a reproducible build is what makes source checkable, and this project already builds that way — the pinned backpackapp/build image, and the byte-identical clean-worktree rebuilds recorded in DEVNET.md. The missing piece is only that the hash has nowhere on the chain to live.

Sequence

  1. Genesis carries programs: [{ id, sha256 }], inside the genesis hash.
  2. The node refuses to start when its --program set does not match.
  3. verify reads the set from genesis and loads the same programs, so re-derivation is self-contained again.
  4. The explorer's caveat shrinks to what remains true: the source is the operator's claim, the binary is the chain's.
  5. Deploy path, if ever: its own ADR.

Steps 1 to 3 are the ADR. Step 4 is a paragraph. Step 5 is a project.

Built, 2026-09-13

  1. Genesis.programs: Vec<(Pubkey, [u8; 32])>, written by solieum-node genesis --program ID=FILE.so (repeatable; the digest is computed from the file by the same parser run uses) as "programs": [{ "id", "sha256" }]. Hashed under the same append-only rule as the portal and the commitment version: appended only when the set is non-empty, sorted by id, count inside, so every chain running today keeps its identity and no set can be written as two.
  2. run refuses, by name, a started set that is not the committed one: a committed program not started, started with other bytes, or a started program the genesis does not commit. One allowance, and only for a chain whose genesis commits nothing: --uncommitted-programs, after which the banner says the chain's identity does not determine its behaviour. Gamma commits nothing and starts nothing, so it is untouched; the local example chains that register Metaplex now need the flag, which is the point.
  3. verify takes the same --program flags, checks them against the genesis with the same function, loads them into its re-deriving node (on every reset too), and refuses to re-derive a chain it was not given the committed bytes for.
  4. getPrograms carries committedCount and a per-program committed, and the explorer's caveat says which of the two it is looking at.

What it still does not make true is unchanged: a committed program is checkable, not provable — its steps stay opaque to the dispute game until the interpreter class (ADR-0008) has its seam into a game.