General and Series executed campaigns — 2026-08-29

Evidence class: executed on a local solana-test-validator against real SBF ELFs, at commit 920df2bdb66ea328852283e21e4d2c68733bac6c. Every number below was measured from a finalized transaction on a private localhost ledger, not from ProgramTest and not from simulation. No devnet, no mainnet, no wallet, and no deployed release identity is claimed. The ledgers were discarded after measurement; the campaigns are reproducible from the recipes here, and one was re-run clean-room to prove exactly that.

Result

Two families that had artifacts and no caller now have callers that ran.

  1. General — all seven authored actions accepted against the real accelerator ELF, at runtime widths 1 and 4. Eleven finalized transactions per run, because the settlement half is a chain rather than seven shots.
  2. Series — the first executed Series Found in this tree's history: one finalized series_consume, plus an on-chain-committed hostile.

Neither reaches Trading's commit half. That is one shared gap, not two, and it is recorded as an open protocol decision in WAVE.md.

General: seven actions, eleven steps, all accepted

local-private-validator-general-hot-campaign-v1 in tools/local-validator/bootstrap/successor.

Seven authored actions occupy eleven steps: Collect and Distribute each consume the three settlement manifest rows, and Materialize sits between them because the collected inventory must exist as a complete set before anything is distributed. Every settlement step reads the cursor the previous one produced, and the whole chain is derived natively — in general_settlement_fixture.rs — before a single transaction is signed, so a broken chain fails on the host.

stepactionACKCUlegacy packet
0Consideraccepted34,825863
1Freezeaccepted31,371797
2InitializeSettlementaccepted60,466920
3–5Collect × 3 rowsaccepted55,703 / 56,908 / 56,885900
6Materializeaccepted51,884866
7–9Distribute × 3 rowsaccepted55,654 / 56,859 / 56,890900
10Closeaccepted60,046885

Also 11/11 accepted at --outcome-count 4 (CU 31,516–61,379). Packets do not move with the width because the register bank travels in scratch pages, so what the 1,232-byte ceiling binds is the account frame, not the bank.

The width is not a preference. Six of the seven serialise to 1,273–1,329 bytes at N=258 against the 1,232-byte ceiling, so a campaign claiming that width would record routes no validator would accept. The caller refuses N=258 explicitly rather than discovering it.

Nothing in the settlement frame is a literal. The verifier cursor, the verified-candidate certificate and the manifests are outputs of a real collection half: a batch opened on a live root, three signed portfolio orders admitted, the batch closed, a candidate addressed by its own digest, a submission funded, and every row through verify_candidate_row_v1.

Recipe

Build the two ELFs, then run two phases — the first opens no socket and no key, because its output is the genesis the validator must start from:

cargo build-sbf --manifest-path programs/dclutch-general-accelerator-sbf/Cargo.toml --sbf-out-dir "$SBF"
cargo build-sbf --manifest-path programs/dclutch-general-accelerator-sbf/test-programs/general-caller/Cargo.toml --sbf-out-dir "$SBF"

# phase 1 — emit 95 genesis accounts and 11 planned journals
dclutch-local-successor-bootstrap local-private-validator-general-hot-campaign-v1 \
  --accelerator "$ACC" --caller "$CALLER" \
  --account-dir "$D/accounts" --journal-dir "$D/journals" --evidence "$D/evidence.json"

solana-test-validator --ledger "$L" --reset --rpc-port 21400 \
  --faucet-port 21402 --gossip-port 21403 --dynamic-port-range 21410-21441 \
  --ticks-per-slot 16 --account-dir "$D/accounts" \
  --bpf-program "$ACC" "$SBF/dclutch_general_accelerator_sbf.so" \
  --bpf-program "$CALLER" "$SBF/dclutch_general_accelerator_test_caller_sbf.so"

# phase 2 — sign, submit, finalize
... --execute --rpc-url http://127.0.0.1:21400 --payer-keypair "$D/payer.json"

The one thing to carry forward

**Five of the General Hot bank identities are route-dependent, and for SOURCE_VAULT_CONTEXT and DESTINATION_VAULT_CONTEXT zero is what the enabled route requires.** An unwritten register does not read as absent; it reads as live. That asymmetry is why a partly-filled bank looks like it is almost working — the actions whose zero defaults happen to be correct pass and the rest refuse — and it cost three validator rounds to find. Write all of them explicitly.

Series: the first executed Found

local-private-validator-series-consume-v1. series_consume is the only Series route this tree dispatches, and it had only ever run inside ProgramTest.

outcomes258
finalized slot60
compute units624,620
account metas62 (61 unique)
instruction data656 bytes
routed wire1,037 bytes (v0 + ALT)

Signature 3Qpa7WSKag8nFQsm8vMcrFbaRqe3M27ajfs1QVAvHgTQ3GVWm3ZbLea67gAFyPRjRZbnb7V2uiEt8Rn9mDanb9F4. Measured draw is below the 722,142 CU recorded in docs/reference/budgets.md.

A write path's acknowledgment is the state it committed, not a returned buffer. The consumer authenticates the Market being Core-owned and written and the founding permit holding exactly 5,122,560 lamports — the two facts the ProgramTest asserts. The journal ladder is planned → prepared → submitted → finalized, each phase renamed into place; rerunning a finished campaign is a no-op, and that check necessarily precedes the vacant-Market preflight, because a succeeded Found is exactly what makes the Market non-vacant.

Why this was hours and not days

The ~1,250-line fixture was not ported. It stays in programs/dclutch-core-sbf/tests/found_program_test.rs with its one author and gained a single #[ignore]-gated emitter, emit_series_consume_validator_campaign, which builds the campaign exactly as every other Series test does, starts the genesis it would have run against, and reads every account the instruction names back out of the banks client. Output: 61 genesis account JSONs (2 absent by design — funding_source and funding_source_replay are deliberately never created) plus a manifest.

Reading rather than reconstructing is what made four hazards dissolve instead of needing solutions:

The fifth, the occurrence's 10,000-slot retry window measured from slot zero, is bought off by running at the default tick rate: ~66 minutes of headroom instead of ~16.

Legacy routing is not tight here, it is impossible: 61 unique keys is 1,952 bytes of addresses against a 1,232-byte packet.

Hostile, committed rather than simulated

--expect-refusal 12293 skips the vacant-Market precondition and requires the transaction to fail with exactly that code. It skips preflight on purpose: a hostile rejected by simulation proves what a simulator thinks, not what the chain did. The double-consume replay committed in slot 535 carrying {"Custom":12293} (CoreSbfError::Market) and left the Found byte-unchanged — the permit still at 5,122,560 lamports. A hostile that succeeds is reported as the loudest possible failure of the property it defends.

Reproducibility

Re-run clean-room — fresh emit, fresh ledger, fresh port — yields the same Market, permit, permit balance, 624,620 CU and 1,037 wire bytes. Only the signature and slot differ.

release_v4: the first assembler, and what it refuses to default

programs/dclutch-trading-sbf/src/series/release_v4.rs.

Series had complete V4 artifact encoders and not one production caller. Nothing assembled them into a descriptor, so authenticate_series_consume_artifacts_v4 — the function deciding whether a Series release is admissible at all — had zero callers of any kind, and the only bundle in the tree was a unit-test descriptor built from placeholder identities (byte_id(10), byte_id(11), …) whose digests deliberately match no real artifact.

This emits the three fully-determined artifacts (account profile, request profile, transition) and builds a descriptor naming every artifact by the digest of bytes the caller is holding. That is the content of "self-consistent" and the precondition for a Market ever selecting the release.

Two artifacts are typed parameters, not defaults, and that is the point.

The requirement the lifecycle must meet is derived off the verifier, in order, as SERIES_CONSUME_LIFECYCLE_REQUIREMENTS_V4 — so a future author can check an answer against the verifier rather than against prose. The fourth conjunct is why this blocks: action_plan_count(Consume) must be nonzero, so a policy covering only Prepare or Expire decodes, validates against the profile, and is still refused.

What is not claimed