Generic Market founding: reachability and the local founding campaign — 2026-08-26

Evidence boundary

This is local-validator evidence only, at evidence-ladder level 4. Every transaction below ran against one solana-test-validator 4.0.2 bound to 127.0.0.1, from a fresh genesis prepared by tools/local-validator/bootstrap/successor. Nothing here is devnet or mainnet evidence, nothing here is a deployment, and no program in it has a checked release manifest outside this lab. The Pyth inputs are the captured synthetic local fixture in PYTH_SYNTHETIC_RELEASE_V1.md; that is a lab projection, never a production provider release.

No formal verification is claimed. Every refusal recorded here is an executed refusal on a specific input, not a proof over all inputs. The CU figures are measured on this validator with these exact artifacts, and are labelled measured-profile, not mathematical.

Superseded on 2026-08-27 by the W1f lane — READ THIS FIRST

The Market is OPEN. DCLTGMF1 executed on a localhost validator at 1,184,132 compute units — Custody LockHoardAndCloseSource, Core Found-and-permit, Custody RealizeAndClose, Claims FoundingV5, and Core Open last, five stages in one rollback domain and one transaction — after DCLTPCB1 completed all four projected-Custody prestate stages at 754,119 CU.

Everything below this heading is the historical record of six earlier campaigns and is kept verbatim, because the reasoning is what located each blocker. Three of its conclusions are refuted by measurement and one of its counts was wrong every time it was stated. The current truth, the current numbers, the current artifact digests, and the current transcript are in the W1f supersession at the end of this document.

The record below says2026-08-27
DCLTPCB1 is heap-bound and no runner can fix itit completes; the grant had to reach the route through its frame, not its transaction
requesting a heap frame "was measured to change nothing"it is the reason the third stage runs at all
the Market is Found, not Openthe Market is Open
the runner must pre-fund three (W1d) / four (W1e) accountsfive, two of them compared for exact equality

Superseded in part on 2026-08-26 by the W1b lane

The Market is now Found. Everything below the "Headline" heading is the historical record of the first campaign and is kept verbatim as such. Two of its findings no longer hold, and the current truth is in the W1b supersession section, which carries the new measurements, the new artifact digests, and the new campaign transcript. Read that section for what is true now; read the rest for how it was found.

ThenNow
Registry activation with the real seven artifacts could not executeexecutes; five transactions, worst role 682,276 CU
Found31 exhausted the 1,400,000 CU maximumexecutes; 234,043 CU, and the Market account exists
The Market is not Open, and it is not Found eitherthe Market is Found. It is still not Open

Superseded again on 2026-08-26 by the W1c lane

Blockers A and B are implemented, and a third structural blocker was found beneath them. The current truth about reachability is in the W1c supersession section. That section adds no on-chain evidence: W1b's transcript and CU figures remain the newest measurements. The Market is still not Open.

W1b saidW1c
Blocker A decided, implementation queuedimplemented, 728299a; frame 139 to 137
Blocker B needs a new Trading verticalimplemented, 28d2da6; DCLTPCB1, 60 accounts
(unrecorded)the projected-Custody caller PDA seed domain was 35 bytes and could never derive an address, so the entire projected family was dead at runtime; fixed f30d087
"plus a funded funding-source vault", as part of Blocker BBlocker C, its own Custody-side vertical: the Lock stage's funding source cannot be created, and cannot be built on the existing normal-custody handlers

Headline

(historical, 2026-08-26, first campaign)

The Market is not Open, and it is not Found either. The campaign reaches canonical Core Found31 and Found31 exhausts Solana's per-transaction maximum of 1,400,000 compute units. With the real seven artifacts bound into the release set, the infrastructure does not even activate. Three separate defects on the path to a first market were found by executing it; two are fixed here, the third is a measurement handed to the lane that owns it.

FindingStatus
Host Found/RentV2 projections refused the real System Programfixed, c25de27
Capability-root selection was a SHA-256 fixed pointfixed, 386f254
Found31 exceeds the 1.4M CU maximum (on-chain full-ELF hashing)fixed, c61376d
Registry activation with the real seven artifacts also exceeds itfixed, c61376d
No route creates a pre-Market Trading capability rootdecided, docs/decisions/0004-founding-capability-root.md; implemented, 728299a
No live route creates the projected-Custody state the outer's Lock consumesimplemented, 28d2da6
The projected-Custody caller PDA seed domain exceeded 32 bytes, so no projected transition could ever signfixed, f30d087 (found by W1c)
No route creates the funding source the outer's Lock consumes and closesopen; needs a Custody-side implementation owner

Artifacts

(historical, 2026-08-26, first campaign; the current artifacts are in the supersession section above)

Built from the working tree at this commit with the pinned toolchain (cargo-build-sbf 4.0.0, platform-tools v1.53, rustc 1.89.0), default release profile, no --lto and no --optimize-size:

cargo build-sbf --manifest-path programs/<NAME>/Cargo.toml --sbf-out-dir target/w1-sbf
ProgramBytesSHA-256
dclutch_registry_sbf.so225,5042554f99725f48d89b3121f1575709760cd5f956db999744a01cba25a00c1788f
dclutch_core_sbf.so1,004,840d5f9c1834eca97d3243a15a6b991cf11952ca716bfb70b5de6641509247b4fb8
dclutch_claims_sbf.so1,073,77637cb05c3fb34bdd47bf195785a90bb1c6935d09bb70f964f99158360187ad5f7
dclutch_trading_sbf.so1,284,664bd2d8441371e93e7a623ede2c080ad1cc2b0fbfda0f6943e3b0494229090a7da
dclutch_resolution_proof_sbf.so463,5769722b3241d252c1a0d51bf5eef178aa6aace963eaa0865da9e89cce54f8fcb8c
dclutch_custody_sbf.so330,4000f2a81fe9a46117a60565d8f65f1a8fafbd9b53706e186304cb96ac2296db0b6
dclutch_rent_sbf.so152,3522bfced4a7a5297796fd5cdaa7d3af1f2254af8a7158b1fd3968c3059f79747ea

cargo build-sbf --lto was attempted and abandoned: dclutch-trading-sbf declares crate-type = ["cdylib", "lib"], and the toolchain refuses LTO for a lib output. The one program that did build with LTO, Registry, came out larger (226,008 vs 225,504 bytes), so LTO would not have moved the compute result below. --optimize-size was deliberately not used: cargo build-sbf documents it as potentially increasing CU.

The campaign uses the real Registry, Core, and Rent artifacts. The Claims, Trading, Resolution, and Custody roles in the release set are bound as distinct immutable Loader-v3 deployments of the Registry ELF. That is not a convenience: binding the real four makes Registry activation itself exceed the compute maximum, as measured below. Nothing in the Found path invokes those four programs, and the generic founding outer that would is unreachable for the reasons below.

Defect 1 — host projections refused the real System Program (c25de27)

dclutch-product-runtime-v2-operator required the built-in System Program observation to carry an empty body:

|| !state.system_program.data.is_empty()

in both src/found.rs::authenticate_runtime_accounts and src/lifecycle_rent_v2.rs. A real Agave 4.0.2 getMultipleAccounts observation of 11111111111111111111111111111111 carries fourteen bytes of NativeLoader metadata (system_program), so every Found31 and RentCreditV2 plan built from a live cluster snapshot refused with AccountAuthority before exporting an instruction. The sibling record-publication planner had already dropped the same requirement in 770610c; these two sites were missed.

The crate's own tests could not catch it because their fixture modelled the System Program as a vacant account. The fixture now carries the exact Agave metadata, and a new adversarial test pins that key, owner, and executable substitution still refuse on both planners.

Defect 2 — the capability-root selection was a SHA-256 fixed point (386f254)

Core's generic founding authenticated the Trading capability root with, among other conjuncts (programs/dclutch-core-sbf/src/generic_founding_v1.rs, authenticate_root):

CapabilityRootSeedsV1 (crates/dclutch-capability-program-contract/src/lib.rs:536) puts selection.config in the seeds, and GenericFoundingRequestV1 carries capability_root in the encoded body. The conjunction therefore demanded

root = PDA(.., config)        config = SHA256(request(.., root))

which is a SHA-256 fixed point. No honest founder can produce one: the route was unsatisfiable for every well-formed artifact, independently of any missing operator or any missing account.

The fix puts the sole root-free selection preimage in the codec that owns the request encoding. GenericFoundingRequestV1::selection_preimage() fixes the Found-and-permit stage and clears only the capability-root span, so the selected config still binds the Market, generation, release set, context, vaults, funding-list identity, widths, rents, and projected revision byte-for-byte, and the commit-last Open stage authenticates against the same root without a second activation. Core hashes that preimage.

dclutch-market-founding-v1-operator now owns the one acyclic construction — config, then selection, then header, then root, then the finalized request — and capability_root_selection_is_acyclic_and_satisfies_core_authentication evaluates exactly the conjunction authenticate_root evaluates, so satisfiability is checked, not asserted. Substituting any other coordinate still moves the root and refuses.

Defect 3 — Found31 exceeds Solana's compute maximum (measured)

The failing transaction, verbatim from the validator:

Program ComputeBudget111111111111111111111111111111 invoke [1]
Program ComputeBudget111111111111111111111111111111 success
Program <CORE>     invoke [1]
Program <REGISTRY> invoke [2]
Program <REGISTRY> consumed 531543 of 537635 compute units
Program <REGISTRY> success
Program <CORE>     consumed 1399850 of 1399850 compute units
Program <CORE>     failed: Computational budget exceeded

Read it as: Core burns 862,215 CU before the Registry role CPI, the Registry reauthentication CPI costs 531,543, Core is left with 6,092 and dies. The compute limit requested was already MAX_COMPUTE_UNIT_LIMIT (1,400,000), so there is no headroom to buy, and the true requirement is strictly greater than what was consumed.

The dominant term is on-chain hashing of whole ProgramData ELFs. Both programs/dclutch-registry-sbf/src/lib.rs:408 and programs/dclutch-core-sbf/src/infrastructure.rs:314 build a DeploymentObservationV1 containing hash(programdata_view.elf()).

The per-byte rate below is inferred from these measurements, not quoted from a specification: the Registry CPI consumed 531,543 CU while hashing 1,004,795 ELF bytes, i.e. about one CU per two bytes. Applying that rate:

ELF hashedBytesPredicted CUHashed by
Core1,004,795~502,400Core, authenticate_immutable_core_release
Registry225,459~112,700Core, authenticate_found
Rent152,307~76,200Core, authenticate_found
Core again1,004,795~502,400Registry, role reauthentication CPI
total~1,193,700

That predicted total is ~85% of the entire per-transaction maximum, and it matches the observed split closely: the Registry CPI's measured 531,543 is ~502,400 of Core-ELF hashing plus ~29,100 of everything else it does.

The Core ELF is hashed twice in one transaction. For an immutable Loader-v3 deployment the (program, programdata, deployment_slot, authority = None) tuple already pins the bytes — the ELF cannot change — so re-deriving the digest per transaction buys nothing the deployment binding does not already give.

That argument is already written down and already adopted elsewhere: dclutch_registry_contract::immutable_release_elf_digest_v1 returns the release's admitted digest when, and only when, the release is Immutable, its recorded authority is None, and the observed on-chain authority is None. programs/dclutch-registry-sbf/src/batch_v2.rs:186 and programs/dclutch-trading-sbf/src/execution_strategy_v2.rs:584 both take it.

The two sites Found31 actually traverses do not:

Adopting the existing batch_v2.rs pattern at those two sites is the whole of the arithmetic above. It belongs to the W2 lane that owns the registry fast path and the 1.4M gate; this lane deliberately did not touch it. The same arithmetic is the likeliest explanation for the ~2.87M CU measured on the common Hot path.

The same rate independently predicts the other large number in the campaign. Registry five-role activation authenticates Core plus four roles that this harness binds to the Registry ELF: 502,400 + 4 × 112,700 = 953,300 predicted against 1,089,297 measured, leaving ~136,000 for everything else activation does. Two independent transactions, one rate, both explained.

That prediction has a sharp consequence, and the campaign was re-run with the real seven artifacts bound into the release set to check it. The five roles then hash Core (502,400) + Claims (536,900) + Trading (642,300) + Resolution (231,800) + Custody (165,200) ≈ 2,078,600 CU of hashing alone, before activation's other ~136,000 — half again over the maximum. Predicted: activation itself cannot execute. Observed:

Program <REGISTRY> invoke [1]
Program 11111111111111111111111111111111 invoke [2]
Program 11111111111111111111111111111111 success
Program <REGISTRY> consumed 1399850 of 1399850 compute units
Program <REGISTRY> failed: Computational budget exceeded

with loadedAccountsDataSize: 4,389,755 — the five real ProgramData accounts. The successor infrastructure cannot be activated at all with its own real artifacts. Every campaign in this document therefore runs with Claims, Trading, Resolution, and Custody bound to distinct immutable Loader deployments of the much smaller Registry ELF; that substitution is what buys the 1,089,297 CU activation, and it is the only reason the campaign gets as far as Found31.

Two failures, two transactions, one cause, and the second was predicted from the first before it was run. The campaign's early refusals cost 5,777 CU (wrong infrastructure authority) and 6,958 CU (substituted lifecycle credit in Found31), because they refuse before reaching the release membrane at all.

W1b supersession 2026-08-26

Same evidence boundary as the rest of this document: one solana-test-validator 4.0.2 bound to 127.0.0.1, fresh genesis, local-validator evidence only. Not devnet, not mainnet, not a deployment, no formal verification claimed. Every CU figure is measured-profile on this validator with these exact artifacts.

What changed

c61376d split the two defects that wore the same symptom.

Recurring readers stopped recomputing an authenticated fact. Registry reauthentication (programs/dclutch-registry-sbf/src/lib.rs) and Core's Found path (programs/dclutch-core-sbf/src/infrastructure.rs) now take immutable_release_elf_digest_v1 through one shared cached_role_deployment_observation, exactly as batch_v2.rs already did. Core gains the same split for its immutable infrastructure profile: first admission in process_initialize still hashes, recurring authenticate_profile does not. The fast path is strictly stronger than the hash it replaces — it requires the immutable policy, an absent recorded authority, and an absent live upgrade authority, none of which the hashing path demanded on its own — and identity, link, ownership, executability, deployment slot, and authority are all still rechecked.

Activation could not be optimised, so it was split. First admission is the one site that checks an artifact record's claimed elf_digest against the bytes actually deployed; a finalized record is attacker-publishable until then. RegistryInstructionV1::ActivateRole therefore admits exactly one role per transaction, so the largest single hash is one artifact rather than five. The activation cache was already an incrementally written, idempotent, alias-checked buffer, and a partially written cache cannot decode, so no reader can consume a half-activated release set.

Artifacts

Built from dcd7ac3 in an isolated git archive HEAD tree with the pinned toolchain (cargo-build-sbf 4.0.0, platform-tools v1.53, rustc 1.89.0), default release profile, no --lto and no --optimize-size.

ProgramBytesSHA-256
dclutch_registry_sbf.so220,728954ebcf92cbbed25e3f22d817f894275a566cf2f4d1903b52bc2cb893e727f79
dclutch_core_sbf.so1,008,4725b75d2f4e358514dc6da1c19911d101416047df1c4d9707dd368981b299f8e1e
dclutch_claims_sbf.so1,074,25666ddc6c9daa23dc022f42be9ed15cd8274de8e791d0cb3d66745ba38e5d849b2
dclutch_trading_sbf.so1,287,34443fb1bad091bd89ef9a6cc9114f15612044ef97b7fe18e441743200dcba3fbb3
dclutch_resolution_proof_sbf.so463,5769722b3241d252c1a0d51bf5eef178aa6aace963eaa0865da9e89cce54f8fcb8c
dclutch_custody_sbf.so330,4405ae26631d815e944d7d55e8d0544fe684b2d01d25909833e009e5858d85260fe
dclutch_rent_sbf.so152,3123486a8197af492317a756e2fce659d399c5e32ff16323edac34fc1f1cafa7b8b

This campaign binds all seven real artifacts. The earlier substitution of the much smaller Registry ELF for the Claims, Trading, Resolution, and Custody roles is gone; it existed only because five-role activation could not otherwise execute.

Measured: activation now fits, one role at a time

RoleELF bytesMeasured CUShare of the 1,400,000 maximum
Core1,008,472549,10839.2%
Claims1,074,256570,88340.8%
Trading1,287,344682,27648.7%
Resolution463,576273,75119.6%
Custody330,440219,44215.7%

The worst single activation transaction is Trading at 682,276 CU. The previous five-role transaction with these same artifacts consumed 1,399,850 and failed with Computational budget exceeded.

The rate inferred in the historical section holds: Trading hashes 1,287,344 bytes for roughly 642,000 CU of the 682,276, or about one CU per 2.0 bytes.

Measured: Found31 executes

slot=1461 fee=5000 compute_units=234043 create canonical Found31 Market

234,043 CU, 16.7% of the maximum, against 1,399,850-and-failing before. The predicted saving was ~1,193,700 CU of ELF hashing; the observed transaction is 1,165,807 CU cheaper than the one that died at the ceiling, which is that prediction to within the noise of what the failed transaction never got to run.

The Market account exists. Both Found31 hostile cases still refuse, and the expensive one got much cheaper because the membrane it had to re-derive before contradicting the Market identity is no longer expensive:

RefusalThenNow
Found31 refuses substituted lifecycle credit6,9586,958
Found31 refuses a substituted Market coordinate and rolls the transaction back829,172141,896

Campaign transcript

Forty-six confirmed transactions in 747 seconds against one validator. Eight lines are hostile cases; each was required to fail and required to leave no poststate behind.

slot=52 fee=5000 compute_units=5777 real-SBF wrong-authority initialization
slot=84 fee=5000 compute_units=226831 real-SBF infrastructure init
slot=116 fee=5000 compute_units=538050 real-SBF activation before revoke
slot=148 fee=5000 compute_units=2520 real-SBF Core Loader revoke
slot=180 fee=5000 compute_units=549108 activate immutable release-set role: Core
slot=212 fee=5000 compute_units=570883 activate immutable release-set role: Claims
slot=244 fee=5000 compute_units=682276 activate immutable release-set role: Trading
slot=276 fee=5000 compute_units=273751 activate immutable release-set role: Resolution
slot=308 fee=5000 compute_units=219442 activate immutable release-set role: Custody
slot=340 fee=5000 compute_units=27020 publish record: Begin
slot=372 fee=5000 compute_units=12848 publish record: Append
slot=404 fee=5000 compute_units=10688 publish record: substituted refund wallet refuses
slot=436 fee=5000 compute_units=19959 publish record: Finalize
slot=468 fee=15000 compute_units=5263 create real Token-2022 collateral and raw-atom wallet
slot=500 fee=5000 compute_units=27022 publish record: Begin
slot=532 fee=5000 compute_units=12850 publish record: Append
slot=564 fee=5000 compute_units=10688 publish record: substituted refund wallet refuses
slot=596 fee=5000 compute_units=20027 publish record: Finalize
slot=628 fee=5000 compute_units=24022 publish Product graph: Product Begin
slot=660 fee=5000 compute_units=11350 publish Product graph: Product Append
slot=692 fee=5000 compute_units=17027 publish Product graph: Product Finalize
slot=724 fee=5000 compute_units=30022 publish Product graph: ResultDomain Begin
slot=756 fee=5000 compute_units=14350 publish Product graph: ResultDomain Append
slot=788 fee=5000 compute_units=23187 publish Product graph: ResultDomain Finalize
slot=820 fee=5000 compute_units=24022 publish Product graph: Portfolio Begin
slot=852 fee=5000 compute_units=11350 publish Product graph: Portfolio Append
slot=884 fee=5000 compute_units=17155 publish Product graph: Portfolio Finalize
slot=916 fee=5000 compute_units=24022 publish record: Begin
slot=948 fee=5000 compute_units=11350 publish record: Append
slot=980 fee=5000 compute_units=17123 publish record: Finalize
slot=1012 fee=5000 compute_units=18020 publish record: Begin
slot=1044 fee=5000 compute_units=8348 publish record: Append
slot=1076 fee=5000 compute_units=11408 publish record: Finalize
slot=1108 fee=5000 compute_units=18022 publish record: Begin
slot=1140 fee=5000 compute_units=8352 publish record: Append
slot=1172 fee=5000 compute_units=8352 publish record: Append
slot=1204 fee=5000 compute_units=8350 publish record: Append
slot=1236 fee=5000 compute_units=12515 publish record: Finalize
slot=1268 fee=5000 compute_units=7121 create Market-scoped lifecycle RentCreditV2
slot=1300 fee=5000 compute_units=10661 create Found31 routing address lookup table
slot=1332 fee=5000 compute_units=11807 extend Found31 routing table page 0
slot=1364 fee=5000 compute_units=8869 extend Found31 routing table page 1
slot=1397 fee=5000 compute_units=6958 Found31 refuses substituted lifecycle credit
slot=1429 fee=5000 compute_units=141896 Found31 refuses a substituted Market coordinate and rolls the transaction back
slot=1461 fee=5000 compute_units=234043 create canonical Found31 Market
slot=1493 fee=5000 compute_units=22977 real-SBF late substituted activation

The late-substitution rollback case that the earlier run never reached now runs: a substituted role ProgramData in a Custody role activation, in a two-instruction transaction whose first instruction is a lamport transfer, must refuse and leave neither the transfer recipient nor any cache mutation behind. It costs 22,977 CU rather than the old five-role frame's cost, because a ten-account activation contradicts the substituted deployment before it hashes.

Why the Market is still not Open

Blocker C is closed. The Open-last chain still needs the atomic outer (DCLTGMF1), and that is gated on the two remaining structural facts.

Blocker A is decided but not implemented. docs/decisions/0004-founding-capability-root.md resolves the lifecycle cycle: the founding capability root is derived by Core from the authenticated Market-selected capability manifest entry and never persisted or read, and the root account is created afterwards by the unchanged ordinary activation route. The historical section below proposed two options and guessed the second was likelier; both were rejected, because the root account is never dereferenced anywhere except authenticate_root and every field of its header is a pure function of the request plus the manifest. The ADR carries the exact wire consequence, the frame arithmetic (139 to 137 accounts), the required refusals, and the file plan.

Blocker B is open, and its shape is not what this document assumed. The historical section names programs/dclutch-trading-sbf/src/projected_custody_composition_v4.rs as the dead family-neutral route. Read at projected_custody_composition_v4.rs:256-264 and :418-433, that module is a Lock adapter: it can emit only LockHoardAndCloseSource, and it requires the projected state to already be in phase HoardOpen. Dispatching to it bootstraps nothing. The real gap is that Custody's Initialize (42 accounts, programs/dclutch-custody-sbf/src/projected.rs:382) and OpenHoard (15 accounts, :490) each require a signing ProjectedCustodyCallerSeedsV1 PDA under the Trading program (:156, :201-205), so they are reachable only by CPI from a Trading dispatch branch that does not exist; and the only in-tree constructor of those two requests, series/projected_custody_v3.rs:85 project_prepare_v3, is Series-shaped and has no non-test caller. A family-neutral bootstrap is a new Trading route, not a wiring change.

Why the atomic outer was unreachable

(historical, 2026-08-26, first campaign; Blocker A is now decided and Blocker C is closed — see the supersession section above)

The outer itself (programs/dclutch-trading-sbf/src/generic_market_founding_v1.rs, magic DCLTGMF1) has the right shape: an 8-byte instruction, four readonly raw-request accounts, and Lock → Found/permit → Realize → Claims FoundingV5 → Core Open last in one rollback domain, 139 account references at three funding states. Two structural facts keep it unreachable even with unlimited compute.

Blocker A — a founding capability root cannot be created

authenticate_root requires header.market() == request.market() (generic_founding_v1.rs:708): the root is keyed to the Market being founded. Core's Found stage simultaneously requires that Market account to be vacant (market.owner == system_program && data_len == 0).

The only in-tree route that creates a Trading capability root is programs/dclutch-trading-sbf/src/outer.rs::process_activation, and its authenticate_market_and_caller (outer.rs:402) requires

market.owner == core_program && market.data_len() == STATE_BYTES

plus CoreState::decode, PDA re-derivation from the decoded identity, and envelope.parent_state_digest() == hash(market bytes). A capability root can only be activated against a Market that already exists as Core state.

capability root <= activation <= existing Core Market <= Core Found <= capability root

Resolving this is a decision about what a founding capability root means, and it is deliberately not made here. The two coherent options:

  1. key the founding root on the sponsoring context rather than the founded Market, and drop header.market() == request.market(); or
  2. add a family-neutral pre-Market activation route whose parent authority is the Registry-selected manifest and the prepaying founder rather than an existing Market state.

Option 2 preserves the root's single-use binding to the exact founding and is the likelier answer, but it is a new signing surface and needs its own adversarial budget. Queued for the Cycle-2 family wave; owner: whoever owns the capability-activation membrane.

Blocker B — nothing can create the projected-Custody state the Lock consumes

execute_lock (generic_market_founding_v1.rs:313) invokes Custody with ProjectedCustodyOperationV1::LockHoardAndCloseSource. Custody's authenticate_common (programs/dclutch-custody-sbf/src/projected.rs:194) requires, for every operation except Initialize, that the state account already be Custody-owned and exactly PROJECTED_CUSTODY_STATE_BYTES_V1 wide. The Lock frame additionally consumes an already-open Hoard vault, a funded funding-source vault, and that vault's replay account.

Only Initialize and OpenHoard can create them, and their caller must be a PDA of request.caller_program under ProjectedCustodyCallerSeedsV1 — only the Trading program can sign them, and only if some live Trading route emits those exact requests. None does:

The outer's first CPI consumes state that no live route can produce. This is a missing first layer of a vertical slice, not a bug in the outer. Queued for the Cycle-2 family wave.

What was checked instead

The outer's cross-request join now has adversarial tests that need no chain (generic_market_founding_v1.rs, request_join_*). The pure coordinate join was split out of authenticate_request_join so it can be exercised without a 139-account frame. The tests construct a canonical four-request join — which by itself demonstrates the join is satisfiable — and then substitute each Claims coordinate the attacker controls (market, founder, hoard, funding source, custody replay, rent credit, release set, custody request digest, generation) and require Content; substituting the named Claims or Trading program requires Release; breaking the Lock→Realize revision sequence requires Content.

This is not the on-chain rollback case the campaign wants. A substituted Claims account proving byte-exact rollback of the whole Lock→…→Open chain needs the route to execute, which Blockers A and B forbid. Saying otherwise would be claiming execution evidence for a pure function.

The demo Market is no longer a placeholder

The campaign now compiles, publishes, and attempts to found a real demo product instead of [0x21; 32]-style filler: a small categorical SOL/USD range-protection Market over the captured local Pyth fixture. Its Realm, Product graph, Source material, recovery policy, and capability manifest are all finalized Registry records on chain; only the Found transaction itself fails, on compute.

Every semantic identifier is a domain-separated digest over dclutch/local-demo-market/v1 || 00 || role || 00 || part… naming the spec it stands for, and the resolution identifiers additionally bind the captured release's adapter_id and synthetic local label from fixtures/pyth/local-upgraded-2026-08-22. The spec object is generated, not hand-written:

cargo run --manifest-path tools/local-validator/bootstrap/successor/Cargo.toml \
  --offline -- demo-market --registry-program-id REGISTRY_ID

This is a lab demo Market. The fixture is synthetic per PYTH_SYNTHETIC_RELEASE_V1.md, the identifiers name specs that no production registry publishes, and nothing about it is a price feed, a product offering, or a deployment.

Found31 does not fit a legacy packet either

Before the compute ceiling there was a packet ceiling. With its keys inline the canonical 31-account Found frame serialises to 1,242 raw bytes — the validator reported the base64 form as too large: 1656 bytes (max: encoded/raw 1644/1232) — so Found31 had never been submitted at all. It misses by ten bytes, which is worth saying plainly: the frame is one 32-byte coordinate away from fitting, and any future coordinate makes it worse. The runner now publishes a finalized address lookup table and submits Found31 as a v0 transaction through dclutch-versioned-message-operator, which owns table admission and packet geometry.

Routing is transaction data, never protocol authority, and the campaign proves it: only non-signer coordinates and the invoked Program are routed, the fee payer and every signer stay in the message's static key list, the table is authority-owned rather than frozen so its rent stays recoverable, and it is usable only strictly after the slot that last extended it. The new hostile case substitutes the Market coordinate under that attacker-chosen routing inside a two-instruction transaction whose first instruction is a lamport transfer; the transaction must refuse and roll back to a fee-only debit, with neither the substituted address, the transfer recipient, nor the canonical Market left behind.

Campaign transcript

(historical, 2026-08-26, first campaign)

Forty confirmed transactions in 690 seconds against one validator, ending in the Found31 refusal above. Every line is a finalized transaction; the eight lines that name a refusal are the campaign's hostile cases, and each was required to fail and required to leave no poststate behind.

slot=50 fee=5000 compute_units=5777 real-SBF wrong-authority initialization
slot=82 fee=5000 compute_units=227728 real-SBF infrastructure init
slot=114 fee=5000 compute_units=541555 real-SBF activation before revoke
slot=146 fee=5000 compute_units=2520 real-SBF Core Loader revoke
slot=178 fee=5000 compute_units=1091088 real-SBF Registry activation
slot=210 fee=5000 compute_units=27022 publish record: Begin
slot=242 fee=5000 compute_units=12849 publish record: Append
slot=274 fee=5000 compute_units=10689 publish record: substituted refund wallet refuses
slot=306 fee=5000 compute_units=19960 publish record: Finalize
slot=338 fee=15000 compute_units=5263 create real Token-2022 collateral and raw-atom wallet
slot=370 fee=5000 compute_units=24022 publish record: Begin
slot=402 fee=5000 compute_units=11349 publish record: Append
slot=434 fee=5000 compute_units=9187 publish record: substituted refund wallet refuses
slot=466 fee=5000 compute_units=17025 publish record: Finalize
slot=498 fee=5000 compute_units=24024 publish Product graph: Product Begin
slot=530 fee=5000 compute_units=11351 publish Product graph: Product Append
slot=562 fee=5000 compute_units=17028 publish Product graph: Product Finalize
slot=594 fee=5000 compute_units=30024 publish Product graph: ResultDomain Begin
slot=626 fee=5000 compute_units=14351 publish Product graph: ResultDomain Append
slot=658 fee=5000 compute_units=23188 publish Product graph: ResultDomain Finalize
slot=690 fee=5000 compute_units=24024 publish Product graph: Portfolio Begin
slot=722 fee=5000 compute_units=11351 publish Product graph: Portfolio Append
slot=754 fee=5000 compute_units=17156 publish Product graph: Portfolio Finalize
slot=786 fee=5000 compute_units=24024 publish record: Begin
slot=818 fee=5000 compute_units=11351 publish record: Append
slot=850 fee=5000 compute_units=17124 publish record: Finalize
slot=882 fee=5000 compute_units=18022 publish record: Begin
slot=914 fee=5000 compute_units=8349 publish record: Append
slot=946 fee=5000 compute_units=11409 publish record: Finalize
slot=978 fee=5000 compute_units=18024 publish record: Begin
slot=1010 fee=5000 compute_units=8353 publish record: Append
slot=1042 fee=5000 compute_units=8353 publish record: Append
slot=1074 fee=5000 compute_units=8351 publish record: Append
slot=1106 fee=5000 compute_units=12516 publish record: Finalize
slot=1138 fee=5000 compute_units=8621 create Market-scoped lifecycle RentCreditV2
slot=1170 fee=5000 compute_units=10661 create Found31 routing address lookup table
slot=1202 fee=5000 compute_units=11807 extend Found31 routing table page 0
slot=1234 fee=5000 compute_units=8869 extend Found31 routing table page 1
slot=1267 fee=5000 compute_units=6958 Found31 refuses substituted lifecycle credit
slot=1299 fee=5000 compute_units=829172 Found31 refuses a substituted Market coordinate and rolls the transaction back

The hostile cases in that transcript, in order:

RefusalCUWhat it pins
real-SBF wrong-authority initialization5,777only the plan-pinned ephemeral authority may create the sole infrastructure profile
real-SBF activation before revoke539,770a Core still carrying an upgrade authority is not an accepted immutable release
publish record: substituted refund wallet refuses (twice)10,689 / 7,688Registry Finalize refunds only the exact staging sponsor
Found31 refuses substituted lifecycle credit6,958Found31 will not accept a foreign account as its Market-scoped RentCreditV2
Found31 refuses a substituted Market coordinate and rolls the transaction back829,172routing is not authority: the Market address is derived from the immutable identity, and the whole two-instruction transaction reverts to a fee-only debit

Not shown, because it runs after the Found stage in the harness and the harness aborts first: the late substituted-ProgramData activation rollback, which the same campaign asserted in earlier runs.

The two Found31 refusals are worth contrasting. The substituted-credit case costs 6,958 CU because the RentCredit coordinate is checked early. The substituted-Market case costs 829,172 CU because the Market identity is only contradicted after Core has already re-derived the release membrane — including the ELF hashing above. A refusal that expensive is itself an argument for the fast path: the cheap check is behind the expensive one.

What an open-market snapshot would unlock, and what to do next

The Market account now exists, so the founder-facing surfaces have a real Core state to read for the first time. What still does not exist is everything the atomic outer would create in the same rollback domain: there is no Claims aggregate, no founder Position, and no Hoard, because Found is not Open.

The ordered path to a first Open market, updated:

  1. ~~Get Found31 under 1.4M CU~~ — done, c61376d. Found31 costs 234,043 CU and the worst activation transaction costs 682,276 CU, both measured above. The saving is structural, not a tuning pass: recurring readers stopped recomputing an authenticated digest, and first admission was split one role per transaction rather than weakened.
  2. Implement the derived founding capability root (Blocker A) — decided in docs/decisions/0004-founding-capability-root.md, with the wire change, the frame arithmetic, the required refusals, and the file plan already written. No route needs to be created: Core derives the root address instead of reading an account, and ordinary activation keeps sole authority over creating the account later.
  3. Add the family-neutral projected-Custody bootstrap (Blocker B) — a new Trading dispatch route that emits Initialize and OpenHoard under a ProjectedCustodyCallerSeedsV1 signer, plus a funded funding-source vault. This is a new vertical slice, not a dispatch wiring change; see the supersession section for why projected_custody_composition_v4.rs cannot serve.
  4. Then drive DCLTGMF1. After Blocker A the frame is 137 account references at three funding states, so it needs the same address-lookup-table routing this lane added, and it will be measured against the same 1.4M ceiling with five CPIs in one rollback domain. Found31's 234,043 CU is the only one of those five stages measured so far.

The on-chain founding-outer hostile case this document wants — a substituted Claims request account proving byte-exact rollback of the whole Lock -> Found -> Realize -> Claims -> Open chain — still needs the route to execute, and therefore still waits on Blocker B. The pure cross-request join tests remain what they were: a pure function checked without a chain, and not execution evidence.

W1c supersession 2026-08-26

This section adds no on-chain evidence. No validator campaign was run for it. Everything below is source-level: implemented routes, unit-tested refusals, and one structural impossibility established by reading the code that decides it. The CU figures and the transcript in the W1b section remain the newest measurements and are unchanged. Read this section for what is now implemented and for why the outer is still not executable; read W1b for what has actually run.

Blocker A is implemented

docs/decisions/0004-founding-capability-root.md was decided at dcd7ac3 with an exact file plan and no implementation: the account counts were still 35 and 24 and authenticate_root still read a root account. 728299a implements all seven steps.

Core now rebuilds CapabilityRootHeaderV1 from facts it has already authenticated and requires the request to name the address that derives from it. The Found stage's funding authentication already decodes the Market-selected manifest, so the derivation shares that decode instead of paying for a second one inside the founding transaction. The Open stage does not re-derive: Found persisted the derived address in the Core-owned permit, and authenticate_permit and authenticate_open_request both require it, so an Open stage that disagreed with its own Found stage cannot be constructed. That is the ADR's "must be impossible" refusal, discharged by construction rather than by a second derivation.

The strengthening the ADR predicted is real and now enforced: manifest, entry_index, kind, and capability_release were previously supplied entirely by the founder and checked only for self-consistency, so nothing bound the selection to the Market's own authenticated capability manifest. The manifest identity is now the one Found authenticated and the kind and release come from that manifest's own indexed entry, as Decision 0003 already required of the ordinary route. The operator no longer accepts the four coordinates as free parameters at all.

Wire and frame, exactly as the ADR specified: the request keeps its 400 bytes and spends its previously unchecked 392..400 tail on capability_entry_index plus reserved bytes decode now requires to be zero; Found goes 35 to 34, Open 24 to 23, and DCLTGMF1 at three funding states goes 139 to 137. The frame-width test in generic_market_founding_v1.rs asserted 139 and now asserts 137.

Unit refusals added: a foreign manifest, an entry index outside the authenticated entry count, a sibling entry's exact kind and release, non-canonical manifest bytes, every nonzero reserved tail byte, and a substituted capability_root in the outer's request_join tests, which had never substituted it at all.

Blocker B is implemented — DCLTPCB1

28d2da6 adds the family-neutral projected-Custody bootstrap the W1b handoff specified. It is one Trading dispatch branch, bound to one terminal LockHoardAndCloseSource request and one founding artifact, driving Custody Initialize (42 accounts) and OpenHoard (15 accounts) under their single-use ProjectedCustodyCallerSeedsV1 signers in a single rollback domain, so a Market is never left holding a replay with no Hoard. The frame is 60 accounts.

The family-neutrality is structural rather than asserted. A replay reaches HoardOpen by exactly two transitions, and Custody's authenticate_next admits a successor only when all thirty of its non-transition fields match the persisted request byte-for-byte. ProjectedCustodyRequestV1::founding_prestate_v1 therefore builds both prestates by functional update from the terminal request, varying exactly the four fields Custody permits to vary. No family, escrow shape, or ticket namespace can enter, and the superseded Series-shaped constructor is deleted.

The found-to-lock conjunction the outer evaluated inline is now a shared predicate both routes call. A prestate this route creates is admissible at Lock because the two routes evaluate the same predicate, not because two constructors agree.

Child CPI metas are built from this route's own authenticated frame — a direct instruction, never an Effect-V3 route adapter, so it never consults a downgraded privilege view. Writable and signer masks are asserted rather than mirrored from the runtime. The Custody program is taken from the Registry-activated release set the request names, so a substituted program cannot receive a Trading-derived caller signature. Neither transition returns data, so the persisted replay is the receipt: it is read back and required to be exactly the poststate of the request just signed.

Defect 4 — the projected-Custody caller PDA could never be derived

The projected family was unreachable for a reason no one had recorded, underneath both blockers above.

PROJECTED_CUSTODY_CALLER_PDA_DOMAIN_V1 was dclutch:projected-custody-caller:v1thirty-five bytes. A Solana PDA seed is capped at thirty-two, so find_program_address refuses every one of the 255 bumps and no address exists. Custody demands that signature for Initialize, OpenHoard, LockHoard, RealizeAndClose, AbortOpenAndClose, and LockHoardAndCloseSource, so every projected-Custody transition was dead at runtime — the Series prepare and consume path, projected_custody_composition_v4, and the atomic outer's own Lock and Realize stages included.

It compiled, unit-tested, and reviewed clean for as long as it existed, because nothing in the tree had ever derived the address. It surfaced the first time a test did: the assertion that each prestate has its own single-use caller authority panicked with Unable to find a viable program address bump seed. This is the sharpest available argument for the project's own rule that a slice must include operator construction and executable evidence — a pure-kernel review of that constant finds nothing wrong with it.

Fixed at f30d087: the domain is now dclutch:proj-custody-caller:v1 (thirty bytes). Static assertions cover every Custody PDA domain, and a test asserts the real precondition on the real seed vector rather than on the constant. Every projected-Custody caller PDA address has moved; any fixture pinning one must be regenerated. A repo-wide sweep of *SEED*/*PDA* byte-string constants found one other over-long domain, not in this lane's ownership and with no seed use today: GENERAL_CANDIDATE_PAGE_PDA_DOMAIN_V1 is thirty-three bytes at crates/dclutch-general-contract/src/lib.rs:120.

Blocker C — the Lock stage's funding source is not creatable, and cannot be built on the existing normal-custody handlers

DCLTGMF1 is still not executable, and the reason is a third structural blocker that this lane established and that the W1b handoff had folded into Blocker B as the parenthetical "plus a funded funding-source vault". It is not a sub-task of Blocker B. It is a Custody-side protocol gap, and the two requirements that create it are mutually unsatisfiable today.

LockHoardAndCloseSource consumes and closes two accounts that projected_custody_bootstrap_v1 deliberately does not touch — the funding source vault at frame index 8 and the funding source replay at index 12:

custody-sbf/src/projected.rs:1213-1250  source vault  = PDA[CUSTODY_VAULT_PDA_DOMAIN_V1,
                                          request.market, release_set,
                                          funding_source_context, compartment]
custody-sbf/src/projected.rs:1226-1245  source replay = PDA[CUSTODY_REPLAY_PDA_DOMAIN_V1,
                                          request.market, release_set,
                                          funding_source_context]
                                        decoded as a NORMAL CustodyReplayV1
custody-contract/src/projected.rs:749-790  replay.market == request.market,
                                          caller_role == Trading,
                                          open_vault_count == 1,
                                          generation == request.generation
custody-sbf/src/projected.rs:971,1257-1271  request.market must be VACANT:
                                          owner == system_program, data_len() == 0

The source must therefore name the very Market being founded, while that Market's account is still a vacant System account. Every route that can write a normal CustodyReplayV1 requires the opposite:

The Market address is a PDA over an identity that includes generation, so a previous market's realized leftovers can never sit at the next founding's address either. A first founding has no reachable prestate for those two accounts.

This must not be resolved by relaxing authenticate_market. That check is what makes normal custody's whole surface a live-Market membrane; widening it to admit a vacant Market would be a permanent enlargement bought to fix one transaction's ordering — the same trade Decision 0004 rejected for activate_capability_child. The shape that fits the existing design is a new projected-family Custody operation that opens a source compartment against a vacant Market and takes market_vacant explicitly, exactly as OpenHoard already does for HoardPrincipal, plus its Trading bootstrap branch. OpenHoard cannot serve: it pins the compartment to HoardPrincipal, which ProjectedCustodyRequestV1::validate explicitly forbids as a funding source, and it writes a ProjectedCustodyStateV1 where Lock requires a CustodyReplayV1.

That is a Custody protocol addition with its own wire, its own frame, its own refusals, and its own owner. It is the last thing between here and an Open Market.

Artifacts

Built from 28d2da6 in an isolated git archive HEAD tree with the same pinned toolchain W1b used (cargo-build-sbf 4.0.0, platform-tools v1.53, rustc 1.89.0), default release profile, no --lto and no --optimize-size. Only the three programs this lane changed were rebuilt; Registry, Claims, Resolution, and Rent are unchanged from the W1b table above. These ELFs have not been deployed or executed — no campaign was run for this section.

ProgramBytesSHA-256
dclutch_core_sbf.so1,007,032c212c8ea3907e2256c441717e538fdf2b6b0fed22e6c2f7836a42883889490d2
dclutch_custody_sbf.so330,4326434093093bf14615e47c58fd2bcef784af05ed7dae1b8b4e5a14f84d3fcf4ac
dclutch_trading_sbf.so1,309,04847931749058f0ee0023836cb6020571c0955ac6632d3d3e1d4a6feeaf7515382

Core shrank by 1,440 bytes: dropping the capability-root account read removed more code than the derivation added. Trading grew by 21,704 bytes for the new bootstrap route.

The path to a first Open Market, corrected again

  1. ~~Get Found31 under 1.4M CU~~ — done, c61376d, 234,043 CU measured.
  2. ~~Implement the derived founding capability root (Blocker A)~~ — done, 728299a.
  3. ~~Add the family-neutral projected-Custody bootstrap (Blocker B)~~ — done, 28d2da6, plus the underivable-seed fix at f30d087 without which neither it nor anything else in the family could have signed.
  4. Make the Lock stage's funding source creatable (Blocker C) — open, unowned, and a new Custody-side vertical as described above. No amount of Trading wiring reaches it.
  5. Then drive DCLTGMF1 at 137 account references over the address-lookup-table routing this campaign already builds, and measure five CPIs in one rollback domain against the 1.4M ceiling. Found31's 234,043 CU remains the only one of those five stages measured.

The on-chain founding-outer hostile case this document has wanted since the first campaign — a substituted Claims request account proving byte-exact rollback of the whole Lock to Open chain — is still not claimed, and now waits on Blocker C rather than Blocker B. The cross-request join tests added in this lane, including the capability-root substitution, are what they have always been: pure functions checked without a chain. They are not execution evidence and are not counted as any.

W1d supersession 2026-08-27

Blocker C is implemented, and a fourth structural blocker was found beneath it and also implemented. This section adds no on-chain evidence: W1b's transcript and CU figures remain the newest measurements, and no campaign was run this lane. The Market is still not Open. Saying so plainly because the lane's gate was an Open Market and the honest answer is that the founding outer became assemblable here, not executed.

W1c saidW1d
Blocker C is a new Custody-side vertical, unownedimplemented, d3ba6a1: OpenSourceCompartment
DCLTPCB1 overflows its SBF verifier frame by 4,480 bytesfixed, d3ba6a1; the whole seven-program build now emits zero frame diagnostics (9258bce)
generic founding's projected_resulting_revision must be 4it is 5
(unrecorded)Blocker D: no route in the protocol could create the FundingState prestate Core's Found stage consumes. Implemented, 2fffe79
the founding outer needs a runnerthe runner still does not exist; the complete frame maps for both routes are recorded below so the next lane does not have to rediscover them

Blocker C is implemented — OpenSourceCompartment

d3ba6a1 adds one projected-family Custody operation. It creates the normal CustodyReplayV1 and the funded source Vault that LockHoardAndCloseSource consumes and closes, against a Market account that does not exist.

authenticate_market is byte-for-byte untouched, and is not on the new path. That was the constraint W1c named and it held. Normal Custody's live-Market membrane still requires data_len() == STATE_BYTES and Core ownership for every ordinary operation. What admits the new transition instead is the projected family's own membrane, which already existed:

custody-sbf/src/projected.rs  authenticate_common      single-use Trading caller PDA
                                                       + persisted ProjectFound projection
                                                       + release reauthentication CPI
                                                       + the exact prior revision
custody-sbf/src/projected.rs  require_vacant_market    owner == system_program, data_len() == 0

require_vacant_market is the inverse of authenticate_market, not a relaxation of it, and OpenHoard already asserted exactly it. The new operation asserts the same thing and adds nothing to the ordinary surface.

The ladder gains one ordered step. Initialize 0→1 and OpenHoard 1→2 are unchanged. OpenSourceCompartment runs 2→3 and moves the phase to SourceFunded, a tag no previously reachable state can hold. LockHoardAndCloseSource now admits exactly two disjoint prestates:

prestatewholocked_amount
HoardOpena family whose principal is already custodied elsewhere — Series escrowmust be 0
SourceFundeda generic founding, whose own OpenSourceCompartment funded the sourcemust equal request.amount

Because SourceFunded is a new value, no state that existed before is admitted by this that was refused before. Series is untouched and still reaches Lock from HoardOpen at revision two.

The replay is the kernel's, not the adapter's. Every field of the minted CustodyReplayV1 is a function of the authenticated request, and its cursor is pinned by SOURCE_COMPARTMENT_REPLAY_REVISION_V1 = 1 rather than chosen — the same value normal_replay_from_realization_v1 mints for the family's other replay. A founder cannot choose what the Lock stage will later read back.

Funding provenance is closed by the request that already existed. Rent for both created accounts comes from request.payer, the same prepaid creation payer that funds the projected replay and the Hoard vault, and both accounts' lamports return to request.rent_credit when Lock closes them. The principal comes from a token account owned by request.refund_owner, who must sign — the same party RefundAndClose pays the principal back to. Whoever may reclaim the principal is exactly who must supply it. No new identity coordinate enters, and ProjectedCustodyRequestV1::validate already forbids HoardPrincipal, None, and External as the source compartment.

Adversarial coverage added at the kernel (crates/dclutch-custody-contract, 23 tests green): a live Market (the inverse of the operation's whole admission), a funder debit that does not equal the principal credited, a pre-funded source vault, a vault that does not end holding exactly the request's principal, a zero replay address, a zero poststate commitment, every other operation attempted at the funded phase, a replayed creation at the same cursor, and the closed-out attempt (AbortOpenAndClose at SourceFunded). The persisted-state round trip is pinned, and the minted replay is shown to be exactly the one the terminal Lock accepts. These are pure functions checked without a chain. They are not execution evidence.

Blocker D — the capability-funding prestate had no creator at all

Found underneath C, by reading what Core's Found stage requires rather than what its Lock stage requires.

core-sbf/src/generic_founding_v1.rs:808-884   one FundingStateV1 per manifest entry
                                              owner == trading_program
                                              data_len() == FUNDING_STATE_BYTES (320)
                                              status == FundingStatus::Pending
                                              manifest_content_id == the authenticated manifest
                                              entry_index == its position
                                              lamports - rent == the manifest's quoted native total
                                              address == CapabilityFundingDerivationV1 PDA under Trading
                                              ordered list digest == request.funding_list_id()
                                              manifest.entry_count() == funding_count, and it may not be zero

The only allocator of such an account anywhere in the repo is programs/dclutch-trading-sbf/src/series/accounts.rs:223 stage_pending_funding. It is Series-shaped — its lamport source is a Trading-owned Ticket — and grep -rn stage_pending_funding returns only its own definition. It has no caller. Every other site (outer.rs:716, general/activation.rs, core-sbf/src/capability.rs) only consumes or validates existing states.

A host cannot supply them either, and this is the part that makes it structural rather than merely missing: a FundingStateV1 is a program-derived address owned by the Trading program. No private key for it exists, so no wallet can sign its creation, and system_program::assign requires the account itself to sign. Only Trading can create one, from inside Trading, under invoke_signed. So a generic founding could not assemble its Found frame at all — this was upstream of every compute or wire concern.

Implemented at 2fffe79 as a fourth DCLTPCB1 stage in the same rollback domain as the other three. Nothing in it is a caller choice:

The bootstrap frame is therefore no longer a constant. It is PROJECTED_CUSTODY_BOOTSTRAP_FIXED_ACCOUNT_COUNT_V1 = 78 plus one account per manifest entry, and the tail's length is asserted against funding_count before anything is created. For the demo Market's three-entry manifest: 81 accounts.

A named new hazard this lane introduced, and why it was chosen

The SourceFunded resting state holds real principal, and no terminal accepts it. AbortOpenAndClose admits only HoardOpen.

That is deliberate. It means the authority over funded principal cannot be destroyed: closing the projection out from under a funded source would strand the principal permanently, because the source replay names a Market that will never become live at that generation. Refusing the abort is the safe direction.

It is still a real liveness hazard, and it is new: before this lane a founder could only strand rent. A founder who runs DCLTPCB1 and never runs DCLTGMF1 has principal that can move only forward, through Lock. The founding is retryable indefinitely — the Lock request is deterministic from the founding artifact and the state rests at next_revision = 3 — so nothing is lost while the founding remains satisfiable.

The closure is a new AbortSourceAndClose terminal at SourceFunded, after expiry, returning the source principal to a refund_owner-owned token account and closing the source vault, the source replay, the empty Hoard vault, and the projection to RentCredit. It is not implemented here and not an extension of AbortOpenAndClose: Series drives that terminal (series/projected_custody_v3.rs:142) and its frame width is fixed, so widening it would reshape a live family's route to serve a different one. Queued, named, and owned by whoever takes the next projected-Custody lane.

The gate, honestly

Not met. No validator campaign was run. The Market is Found, not Open. The Claims aggregate, the founder Position, and the Hoard still do not exist anywhere on any chain. The on-chain founding-outer hostile case — a substituted Claims request account proving byte-exact rollback of the whole Lock→Found→Realize→ Claims→Open chain — is still not claimed, and now waits only on a runner.

What changed is that it no longer waits on a protocol gap. Every prestate the outer's five stages consume now has exactly one live route that creates it:

prestatecreatorstatus
capability rootderived, never createdADR-0004, 728299a
projected replayDCLTPCB1 stage 128d2da6
Hoard vaultDCLTPCB1 stage 228d2da6
source vault + source replayDCLTPCB1 stage 3 → Custody OpenSourceCompartmentd3ba6a1
FundingState × manifest entriesDCLTPCB1 stage 42fffe79
LifecycleRentCreditV2the campaign's existing RentV2 stagealready live
Claims aggregate / Position / admissionthe Claims stage itself allocates themalready live — but see below

One thing the runner must do that is easy to miss: Claims FoundingV5 allocates the aggregate, the founder Position, and the admission with System::allocate + assign only. It never transfers lamports. The runner must pre-fund those three vacant program addresses so that each holds at least rent.minimum_balance(width), or the founding refuses inside Claims. A plain System transfer to a derived address does that; no protocol route is needed.

What the runner has to build, exactly

Recorded here because it is the whole remaining distance and it was expensive to establish. Two transactions, both requiring an address lookup table.

DCLTPCB1 — 81 accounts at three funding states. Eight-byte instruction data, no payload. Layout: two readonly raw-request accounts (the 400-byte founding artifact and the 768-byte terminal Lock request), the Custody program, the 42-account Initialize sub-frame (whose indices 11..41 are Core's 31-account ProjectFound sub-frame, all forwarded readonly and all mutually distinct), the 15-account OpenHoard sub-frame, the 18-account OpenSourceCompartment sub-frame, then one FundingState account per manifest entry. 49 distinct keys, well inside the 64-account lock limit. Exactly four must be writable-and-or-signer at the transaction level: the projected state, the payer (signer), the two vaults, the source replay, the funding tail, and — as signer only — the principal refund_owner.

DCLTGMF1 — 137 accounts at three funding states (134 + funding_count). Eight-byte instruction data. Four readonly raw-request accounts (400-byte founding artifact, 768-byte Lock, 768-byte Realize, 832-byte Claims request), then Lock (14), Found (34 + funding_count + 15), Realize (12), Claims (32), Open (23). Eleven distinct keys must be writable: the projected replay, the rent credit, the Hoard vault, the source vault, the source replay, the Found caller PDA, the Market, the Core permit, and the Claims aggregate, Position, and admission. No account in the frame may be a transaction-level signer — every stage's signer is a PDA signed by invoke_signed — so the fee payer must be a 138th key that appears nowhere in the frame.

Three derivations the runner must get exactly right, because each is a caller-PDA seed input and a wrong one produces an address for which no signature exists:

context_digest       = sha256(b"dclutch:projected-hoard-context:v1" || found.context())
funding_source_context = found.context(), undigested
projection_receipt_digest = sha256(ProjectFoundReceiptV1 bytes)

The third is derivable off-chain without a transaction: the receipt is a pure function of facts the runner already holds (market, generation, realm, mint, token program, collateral release, product record, product, source, release set, rent program, and the digest of the encoded Found request). It does not require simulating the CPI.

Chain-derived rent facts, not choices: state_rent_lamports = minimum_balance(808); vault_rent_lamports = funding_source_vault_rent_lamports = minimum_balance(165); funding_source_state_rent_lamports = minimum_balance(288); each FundingState holds minimum_balance(320) plus the manifest entry's quoted native total.

And two request-shape pins that will otherwise fail late: the terminal Lock must carry expected_revision = 3 and funding_source_replay_revision = 1, and the founding artifact must carry projected_resulting_revision = 5.

Artifacts

Built from 2fffe79 in an isolated git archive HEAD tree with the same pinned toolchain (cargo-build-sbf 4.0.0, platform-tools v1.53, rustc 1.89.0), default release profile, no --lto and no --optimize-size. These ELFs have not been deployed or executed — no campaign was run for this section.

The seven-program build emits zero frame diagnostics. It emitted twenty at ae5c93b (process_projected_custody_bootstrap_v1, 8,576 bytes estimated against a 4,096 maximum), and twenty again mid-lane for the new Custody operation before it was split across three verifier frames.

Built from 05656a3, not from this lane's last commit. bd06c53 and every commit back to ee1dc7d cannot build four of the seven programs from a clean checkout: ee1dc7d committed a use dclutch_capability_seal_contract::{…} into crates/dclutch-account-profile-contract/src/lifecycle_v3.rs while the matching Cargo.toml dependency line was still only in the shared working tree. That is another lane's defect and it is repaired at 05656a3, so these figures are from the first commit at or after this lane's work that builds. They therefore also contain other lanes' concurrent changes to Claims, Core, Trading, and Resolution; only dclutch_custody_sbf.so is attributable to this lane alone, and its digest is byte-identical to an isolated build of this lane's commits.

ProgramBytesSHA-256
dclutch_registry_sbf.so220,728954ebcf92cbbed25e3f22d817f894275a566cf2f4d1903b52bc2cb893e727f79
dclutch_core_sbf.so1,007,096c6373ba564e9c7230409eb549143b83998d3b038fbead9dfc08732caf450edb3
dclutch_claims_sbf.so1,074,25679869b5dec2d60e961c3ac9f9ff5d39780a69bf492cbff35cf393c79fd597f80
dclutch_trading_sbf.so1,333,06444c15378aa892ad7aa3962302fa8ee28c376ef5e0e2d4e2a2f03e7a770a30bc1
dclutch_resolution_proof_sbf.so463,576ae18567499be52880db335f06ba1e00596e7489f1297812e6796cb3e3df1c4d9
dclutch_custody_sbf.so347,53683eb5121559f1d41f75a9e47a4cdfd7cb8927236d8079ba42c8eee032b0195f9
dclutch_rent_sbf.so152,3123486a8197af492317a756e2fce659d399c5e32ff16323edac34fc1f1cafa7b8b

Custody grew by 17,104 bytes over W1c's 6434093093bf… for the new operation; Trading grew by 24,016 bytes over that section's figure for the fourth bootstrap stage and the funding staging, though that delta is not attributable to this lane alone.

Gates this lane ran

Filtered, and stated with their controls rather than as a suite-wide green.

GateResult
cargo test -p dclutch-custody-contract24 passed — the kernel's whole adversarial surface for the new operation
cargo test -p dclutch-custody-sbf --lib7 passed — frame widths are operation-exact, including the new eighteen
cargo test -p dclutch-trading-sbf --lib236 passed
cargo clippy --lib -- -D warnings on all threeclean
cargo fmt --check on all threeclean
cargo build-sbf, all seven programsexit 0, zero frame diagnostics
the successor runner (cargo test, its own workspace)16 passed

The --all-targets clippy on dclutch-custody-sbf and dclutch-trading-sbf fails on pre-existing test-target lints this lane did not touch (programs/dclutch-custody-sbf/tests/program_test.rs:645 needless_borrow, and indexing_slicing/assertions_on_constants across dclutch-trading-sbf's integration tests). Recorded rather than fixed: they are not this lane's and fixing them would put another lane's files in this lane's commits.

W1e supersession 2026-08-27

W1d's verdict was refuted, and the gap it missed has since been closed by another lane. A fifth structural blocker did remain — Core's Found stage and Claims FoundingV5 required the same account to be two different records — and it is recorded below as this lane found it, because the reasoning is what located it. It is fixed at dba22b5/712490d/d163c32 by the Claims lane, along exactly the line the V3 reader's own documentation prescribed, and this campaign already publishes the record the fixed path wants.

What this lane leaves behind: DCLTPCB1 built chain-derived and executed on a real localhost validator — the first on-chain evidence the four-stage projected-Custody bootstrap has ever had — plus a measured heap bound on it that no static reading had found. DCLTGMF1 is still not built. That is now a runner gap and no longer a protocol gap.

W1d saidW1e
no protocol gap remains on the path to an Open Market; only the runner does not existfalse at the time: Core's Found stage and Claims FoundingV5 required the same account to be two different records. Fixed since, at dba22b5
the runner has to build two transactionsone of them is built and executed; the other is now satisfiable and still unbuilt
(unrecorded)DCLTPCB1 is heap-bound, not compute-bound, and no static reading found it
the runner must pre-fund three vacant Claims addressesfour: the Found caller-authority PDA is also the Core Found payer
DCLTPCB1 at 81 accounts, 49 distinct keysconfirmed by construction
(unrecorded)the founding cannot reuse the Found31 Market; it needs its own generation
(unrecorded)the principal supplier cannot be the rent payer
(unrecorded)the demo Market never published the liability basis its Product declares

Blocker E — the liability basis has two incompatible authorities

Found by building the Claims stage's request bytes and discovering that the record Core commits to and the record Claims demands cannot be the same file.

Core's generic Found stage authenticates the suffix linked_basis_raw / linked_basis_staging pair through authenticate_product_basis_v3 (crates/dclutch-product-runtime-v2-svm-reader/src/lib.rs:377-434), called at programs/dclutch-core-sbf/src/generic_founding_v1.rs:891-900:

raw PDA      [b"dclutch-raw-record-v1", GRADED_BASIS_RECORD_SCHEMA_ID_V3, sha256(raw)]  under REGISTRY
raw owner    the Registry program
body         ProductBasisV3, magic DCLTPAY3
identity     sha256(b"dclutch/product-basis/semantic/v3" || raw[..32] || raw[96..])
             must equal the Product record's liability_basis_id

Core then writes both of those into the ClaimsFoundingRequestV5 it commits to inside the one-shot permit (generic_founding_v1.rs:919-920, :1426-1427): linked_basis_record_digest = sha256(that V3 account's data), and semantic_basis_id = that V3 semantic identity.

Claims FoundingV5 authenticates its own account 8/9 pair through authenticate_runtime_product_basis_core_v2 (programs/dclutch-claims-sbf/src/founding_v5.rs:736-761 into affine_batch_v2.rs:557-590, whose record check is liability_basis_v2.rs:925-984):

raw PDA      [b"dclutch-raw-record-v1", LIABILITY_BASIS_SCHEMA_RELEASE_ID_V2, digest]  under CORE
raw owner    the Core program
digest       must equal request.linked_basis_record_digest()  -- the V3 digest above
body         LinkedBasisRecordV2, magic DCLTLNK2, length exactly 224 or 248
identity     sha256(b"dclutch/lbv2/semantic-id/v2" || v2prefix || v2suffix)
             must equal request.semantic_basis_id()  -- the V3 identity above

Equal digests mean equal bytes. Those bytes would have to begin with DCLTPAY3 and DCLTLNK2, be 256 bytes and 224-or-248 bytes, live at a Registry-derived address and a Core-derived one, and satisfy two domain-separated SHA-256 identities — all at once. Unsatisfiable. No frame, no lookup table, and no compute budget reaches Core's commit-last Open through this pair.

Blocker E is fixed

dba22b5 (with 712490d and d163c32) makes Claims FoundingV5 and its three sibling routes authenticate the basis through authenticate_product_basis_v3 against the Registry-owned V3 record — exactly what Core commits — and deletes the legacy LinkedBasisRecordV2 path from the founding route. No identity and no frame width moved, and this campaign was already publishing the right record, so nothing in the bootstrap had to change.

That is the direction this section argued for, and it is worth being explicit that it is the only terminating direction: a LinkedBasisRecordV2 is a Core-owned raw record at a Core-derived PDA, and nothing in the repo creates one for a founding. Teaching Core to admit V2 would have required inventing a creator for a record type the successor had already replaced.

It was already named as debt, with an owner and a remedy. From the V3 reader's own module documentation (crates/dclutch-product-runtime-v2-svm-reader/src/lib.rs:12-17), verbatim:

Follow-on convergence is mechanically limited to the remaining legacy LinkedBasisRecordV2 Claims consumers: src/{affine_batch_v2,liability_basis_v2,rational_representation_v2}.rs and program-test/affine-batch/src/lib.rs under dclutch-claims-sbf, plus dclutch-liability-basis-v2-kernel::{src,tests}/product_claims.rs. They must consume this V3 authentication result rather than add another basis decoder.

So this is exactly the parallel legacy/current authority path the project method forbids, surfaced by the first route that needs both halves inside one transaction. Generic founding is that route. The remedy is the one the reader already prescribesaffine_batch_v2 consumes authenticate_product_basis_v3 against the Registry-owned V3 record and stops deriving a Core-owned V2 record — and it belongs to a Claims lane. This lane owns the runner, generic_market_founding_v1.rs, and the bootstrap tree, and deliberately did not widen either side: the fix deletes the V2 basis path from the founding route; it does not teach Core to admit V2.

Named consequence for whoever takes it: LinkedBasisRecordV2 records are Core-owned raw records at Core-derived PDAs, and nothing in the repo creates one for a founding. Even if the two decoders agreed, that record has no creator — so the convergence is the only direction that terminates.

Defect 5 — the demo Market's liability basis did not exist

Upstream of Blocker E and repaired here at 4b12ee1, because Core requires it under either resolution.

The Product record has always carried a liability_basis_id, and the demo Market has always filled it with a domain-separated name. Found31 does not read it, so nothing noticed. Core's generic Found stage reads it and refuses unless a canonical ProductBasisV3 exists whose semantic identity is exactly that value and whose Product, result-domain, coordinate-domain, and result-unit links match the authenticated graph.

The run spec now carries the record itself. The join is acyclic rather than a fixed point: the V3 semantic preimage omits the Product and result-domain links (raw[..32] || raw[96..]), so the identity is derivable before the Product that declares it exists. The demo derives it from a placeholder-linked record, compiles the Product against it, recompiles the record with both real links, and asserts the identity did not move. CategoricalQ1 is forced, not chosen: a categorical Product admits unit payout scale, no knots, no graded terms, and one basis claim per outcome, and Core separately requires basis.payout_scale == request.basis_scale.

Four corrections to W1d's runner brief, each paid for by building against it

  1. **The Found sub-frame's index 0 is the payer and the Trading caller PDA.** FoundAccounts::parse requires index 0 signer-and-writable, and the outer's invoke_child marks index 0 signer-and-writable — one slot, both roles. So the pre-funding list is four, not three. The bite is small in practice (market.lamports() must equal rent.minimum_balance(352) exactly, so the Found rent top-up is zero) but the slot is still a real payer.
  2. DCLTPCB1's ProjectFound sub-frame payer slot cannot be the bootstrap payer. parse_project requires that slot non-signer and non-writable while the bootstrap payer is a transaction-level signer and writable, and Solana grants privileges per key, not per index. It must be a distinct funded readonly key, and it must actually hold at least the Market rent, because the kernel debits payer_lamports by the Market rent top-up even in projection mode (crates/dclutch-market-core-codec/src/generated.rs:809-815).
  3. The founding cannot run against the Market Found31 created. Every projected-Custody stage asserts require_vacant_market, and Core's project requires the Market vacant. A campaign that keeps the Found31 evidence must found at a different generation, whose Market PDA is a different, still vacant address, with its own LifecycleRentCreditV2.
  4. The principal supplier cannot be the rent payer. OpenSourceCompartment requires funder_owner.is_signer && !funder_owner.is_writable while payer must be writable. Same key, contradictory privileges. So the founding names a separate beneficiary — which is also what credit.refund_wallet() == request.beneficiary() and lock.refund_owner == found.beneficiary() already required — with its own Token-2022 source wallet.

Also confirmed, against FT's report: cargo build-sbf on dclutch-trading-sbf at ff02df0 emits zero overwrites values in the frame diagnostics, both from an isolated git archive HEAD tree with a dedicated target directory and in the gauntlet's own shared-target build log. The two diagnostics FT saw in authenticate_and_project do not reproduce at this revision.

Blocker F — DCLTPCB1 does not execute, and no runner can make it

This is the new on-chain result of this lane, and it is a refusal to execute, not an execution. Three campaigns were run on a real solana-test-validator 4.0.2. The third reached DCLTPCB1 with every prerequisite satisfied and the route failed:

Program log: Error: memory allocation failed, out of memory
Program <trading> consumed 561,101 of 1,399,700 compute units
Program <trading> failed: SBF program panicked

The inner logs place it exactly. Two of the four stages complete first:

stageCUoutcome
Custody Initialize, including the Core ProjectFound CPI340,799executed
Custody OpenHoard, including Token-2022 InitializeAccount3109,545executed
Custody OpenSourceCompartmentnever started — heap exhausted
Trading total at death561,101 of 1,399,700838,599 CU unspent

So the four-stage ladder is heap-bound, not compute-bound, with sixty percent of the compute budget untouched. That is a fact no static reading found: W1d's frame-diagnostic gate was clean, the SBF verifier is satisfied, and cargo build-sbf emits zero diagnostics. The bound only appears at run time.

The obvious workaround does not work, and this was measured rather than assumed. Requesting a 256 KiB heap frame changed nothing: the transaction carried the instruction, the runtime accepted it — the failing instruction index moved from 1 to 2, so both compute-budget instructions were processed — and the route died at the same place. The two halves of "the heap" are not connected:

solana-program-entrypoint-3.1.1/src/lib.rs:39   pub const HEAP_LENGTH: usize = 32 * 1024;
solana-program-entrypoint-3.1.1/src/lib.rs:226  BumpAllocator { ... len: $crate::HEAP_LENGTH }

RequestHeapFrame enlarges the region the runtime grants; the stock BumpAllocator is constructed with the compile-time constant and never asks how much it was actually given. Every dClutch program uses that entrypoint, so no transaction-level declaration can move this bound for any route in this repository. The request was withdrawn rather than left in place — it cost compute on every transaction, shifted every measured figure, and looked like a fix.

Owner and remedy. This belongs to programs/dclutch-trading-sbf/src/projected_custody_bootstrap_v1.rs. The route holds three stages' worth of allocations live against an allocator that never frees — each stage's encoded 768-byte request, its CPI meta vector, and a forwarded AccountInfo vector for a 42-account sub-frame — so its peak is the sum, not the maximum. It is the same shape as the verifier-frame pressure W1d split across three functions, one level up in the heap. Either the route allocates less, or the program supplies its own global allocator over the granted heap. DCLTGMF1 drives five stages through the same allocator over a wider frame, so it should be assumed to have the same bound until measured.

Campaign transcript

Three runs, all on one solana-test-validator 4.0.2 bound to 127.0.0.1 from a fresh genesis, driven by tools/gauntlet/run.sh --mode full. Wall-clock timings are not reported: a concurrent workspace test held the machine at load ~48 throughout, which distorts duration and not compute.

runcommittransactionsoutcome
1a99ffbb60died in this lane's own hostile path: both adversarial cases reused the honest frame, which needs the principal supplier's signature, but went through an expected-failure path with no signer list, so they failed to sign locally and never reached the chain. Fixed at 792496e.
2792496e62both hostile cases reached the chain and refused; honest DCLTPCB1 died out-of-memory at 527,665 CU
351e40aa62identical failure with a 256 KiB heap frame requested — the measurement that withdrew the workaround

Measured compute, run 3 (the Claims ELF differs from runs 1-2, which carry the pre-dba22b5 basis path):

transactionCU% of maximum
Core infrastructure profile initialization232,83116.6%
release activation — Trading717,49651.3%
release activation — Claims573,64941.0%
release activation — Core551,62639.4%
release activation — Resolution264,95618.9%
release activation — Custody229,49116.4%
canonical Found31 Market creation232,53716.6%
founding generation's LifecycleRentCreditV27,3270.5%
DCLTPCB1 refuses a non-terminal request16,3961.2%
DCLTPCB1 refuses a reordered FundingState tail561,60740.1%
DCLTPCB1, honestfailed, out of memory at 561,101

Found31 is no longer the obstacle it was. It creates the Market at 232,537 CU, 16.6% of the maximum; earlier prose in the bootstrap README and REMAINING_OPEN_SEAM said "about 247,000" and, before that, that it exhausted the maximum outright. Corrected to the measurement.

One adversarial case was passing for the wrong reason

The reordered-FundingState-tail case refuses, and its refusal is not yet attributable to the manifest binding it claims to test. In run 2 it consumed 527,965 CU against an out-of-memory death at 527,665; in run 3, 561,607 against 561,101. It is refusing on the allocator, in the same place the honest transaction dies, and it would have read as evidence for the funding-tail check.

Recorded rather than quietly repaired, because the failure mode generalises: a refusal whose compute profile matches an unrelated crash is not evidence about the coordinate under test. The discriminator this case needs is the honest transaction succeeding with an identical frame shape, which cannot happen until Blocker F is fixed.

The other case is sound and is genuine on-chain evidence: DCLTPCB1 refuses a well-formed but non-terminal projected-Custody request at 16,396 CU, refusing in decode_projected_request before any CPI and nowhere near the allocator.

What tranche-A family campaigns may now assume on chain

Nothing new. The Claims aggregate, the founder Position, the Hoard, the projected replay, and the funded source compartment still do not exist on any chain. What is now known, rather than assumed:

W1f supersession 2026-08-27 — the Market is OPEN

Same evidence boundary as everything above and no other: one solana-test-validator 4.0.2 bound to 127.0.0.1 from a fresh genesis, driven by tools/gauntlet/run.sh --mode full at cd05331, local-validator evidence only, not devnet, not mainnet, not a deployment. No formal verification is claimed and every refusal below is an executed refusal on a specific input, not a proof over all inputs. Every CU figure is measured-profile on this validator with these exact artifacts.

Read this section for what is true now. Everything above it is kept verbatim as the record of how it was found, including the three verdicts this run refutes.

ThenNow
DCLTPCB1 is heap-bound and no runner can fix it (W1e)it completes all four stages, 754,119 CU
RequestHeapFrame "was tried and measured to change nothing"it is the reason the third stage runs at all
DCLTGMF1 is satisfiable and unbuilt; the Market is Found, not Openthe Market is Open, in one transaction, 1,184,132 CU
the pre-funding list is four (W1e), three (W1d)it is five
the reordered-FundingState case refuses for the wrong reasonit refuses 68,921 CU short of a succeeding honest transaction

Blocker F is gone, and it was never program-side

W1e's diagnosis of the out-of-memory death was right about the wall and wrong about the remedy, in a way worth stating precisely because the reasoning was sound at the time. It concluded:

RequestHeapFrame enlarges the region the runtime grants; the stock BumpAllocator is constructed with the compile-time constant and never asks how much it was actually given. Every dClutch program uses that entrypoint, so no transaction-level declaration can move this bound for any route in this repository.

Both sentences were true of the tree that measured them. 9abed0c then gave Trading its own entrypoint and its own allocator, one that bumps upward against a ceiling it re-derives from the instructions sysvar under agave's own sanitize_requested_heap_size, and put exactly two routes on the list allowed to lift it: DCLTGMF1 and DCLTPCB1.

That still did nothing, and the reason is the finding this lane contributes before any measurement. admit_heap_frame_v1 is reached only from lift_declared_heap_profile_v1, which locates the instructions sysvar by scanning the top-level instruction's own account list (entrypoint_adapter.rs:660-666). Neither founding route presented it. The adapter's own documentation had already named the consequence — without the sysvar in the frame "the declaration is inert and the route keeps the default ceiling" — and both frames are exact-width, so an appended account is a refusal rather than a no-op. The grant was a wire fact, not a transaction fact.

9d45056 adds one authenticated readonly slot to each route's fixed prefix, ahead of the variable tail, with one shared authenticator so the two cannot drift about what it holds. It is not a security boundary — the adapter re-derives the grant from the sysvar's own bytes and a wrong account there simply means no lift — it is a fail-closed frame assertion, so a frame that cannot deliver the heap the route is declared to need refuses up front instead of running out of memory partway through a rollback domain. Frame widths move by one: DCLTPCB1 78 → 79 fixed (82 for the demo Market), DCLTGMF1 134 + funding_count135 + funding_count (138).

Measured: DCLTPCB1 completes, and it is neither heap-bound nor compute-bound

Run 4 (0ca334d) put the grant in front of the route for the first time. The third stage executed 104,029 CU of real work where it had previously never started, and refused on something else entirely — see the next section. Run 5 (cd05331) is the honest completion:

stagerun 3run 4run 5
Custody Initialize, incl. Core ProjectFound340,799370,696executed
Custody OpenHoard, incl. Token-2022 InitializeAccount3109,545107,958executed
Custody OpenSourceCompartmentOOM, never started104,029, refusedexecuted
Trading FundingState staging × 3never reachednever reachedexecuted
whole routedied at 561,101died at 702,864754,119 of 1,399,700

645,581 CU of the budget is unspent at completion.

Blocker G — the seventh layer, and it had been hiding under the heap wall

Run 4's third stage refused Custom(1) = CustodySbfError::AccountFrame after 104,029 CU. authenticate_source_creation_frame returns Replay or TokenState, and require_vacant_market was satisfied, so one conjunct is left: funder_owner.key.to_bytes() != request.refund_owner (programs/dclutch-custody-sbf/src/projected.rs:1027).

The frame presented the beneficiary as the principal's owner while derive_founding_coordinates set refund_owner to the payer. W1e's own correction 4 had already established that these must be different keys — Custody requires the owner to sign while staying non-writable and the creation payer to be writable, and Solana grants privileges per key — but the artifact was still built with the payer in the beneficiary slot. It could not have been found before this run: the route never reached the stage that checks it.

cd05331 names the beneficiary once and threads it through all four places that were already required to agree: the founding artifact's beneficiary, the terminal Lock's refund_owner, the owner of the Token-2022 wallet the principal comes from, and the lifecycle credit's refund_wallet.

Two further defects in the same commit, found by reading what the founding has to predict rather than by running it:

The pre-funding list is five

W1d said three. W1e corrected it to four. It is five, and two of the five are compared for exact equality, so over-funding refuses as surely as under-funding:

accountrequirementsource
Marketlamports == rent.minimum_balance(352) exactlycore-sbf/src/generic_founding_v1.rs:775-777
one-shot permitlamports == request.permit_rent() exactly, and that must equal rent.minimum_balance(608):778-779, :1135
Claims aggregate>= rent.minimum_balance(256 + 8·claim_count)claims-sbf/src/founding_v5.rs:795-801
founder Position>= rent.minimum_balance(128 + 8·claim_count)same
Claims admission>= rent.minimum_balance(512)same

Core allocates the Market and the permit and Claims allocates its three with allocate + assign only; neither program ever transfers a lamport into them. W1e's fourth entry, the Found caller-authority PDA, needs nothing after all: the Market being exactly rent-funded makes the kernel's rent_top_up zero, and found.rs:571 skips the payer transfer entirely.

And all three Claims balances are digest-bearing. Core reads aggregate.lamports(), position.lamports(), and admission.lamports() at the Found stage and folds them into the ClaimsFoundingRequestV5 it commits to inside the permit (generic_founding_v1.rs:1228-1230). A pre-funding one lamport off does not overpay; it moves a digest and refuses at Claims. That makes the pre-funding transaction part of the founding's authenticated prestate rather than a convenience.

What the runner had to derive, and how it avoided authoring a digest

The founding commits to values that do not exist yet, and the discipline this campaign has kept — the compiler and chain-derived operators own every digest — had to survive that. It does, in this order:

  1. The Lock receipt comes from running the Custody kernel's own lock_hoard_and_close_source over the chain's actual SourceFunded projection and its actual normal source replay, both read back from the accounts DCLTPCB1 left.
  2. The Realize request is the terminal Lock with exactly its operation and its two revisions moved — the same derivation the outer's authenticate_projected_sequence evaluates.
  3. The Realize receipt needs the candidate Core state the Found stage will write. Every field of that state is fixed by the kernel's found — phase Founding, readiness Prepaid, zero terminal winner, zero outstanding capabilities, the identity, the rent credit — so it is constructed, and the encoding is cross-checked by re-encoding the Found31 Market's own decoded state and requiring the bytes the chain is holding.
  4. The permit intent, assembled exactly as Core's build_permit_plan assembles it, and its digest.
  5. The Claims request, which carries that digest, and whose own digest is a seed of the Claims caller PDA.

Acyclic throughout, and every step consumes only what the previous ones produced.

Measured: DCLTGMF1, five stages, one rollback domain

1,184,132 CU, 84.6% of the 1,400,000 maximum. Per stage, from the inner logs:

stageprogramCU
Lock — LockHoardAndCloseSource (Registry reauth 27,562; Token-2022 TransferChecked 1,720 and CloseAccount 1,275)Custody105,722
Found and permit (Registry reauth 48,071; four System calls)Core414,957
Realize — RealizeAndClose (Registry reauth 27,562)Custody87,222
Claims FoundingV5 (four Registry reauths; six System calls)Claims260,279
Open, commit-last, plus the outer's own five joinsCore + Trading315,652

The last row is arithmetic, not measurement: the RPC truncated the log before the Open stage's own accounting line, so it is 1,184,132 − 300 (two ComputeBudget instructions) − 868,180 (the four measured stages). The Registry reauthentication inside the Open stage is visible at 48,071 and is part of it.

There is no per-stage heap figure and this document will not invent one. Neither founding route carries heap checkpoints — the hot_cu_checkpoint! instrumentation is hot_v3's — so what is measured about the heap here is exactly one thing, and it is decisive: with the grant reaching the route, two stages that had never executed do, and nothing anywhere in the chain reports memory allocation failed.

The founding-outer hostile case

DCLTGMF1 refuses a substituted Claims request and rolls the whole founding back, 33,594 CU, InstructionError [3, Custom(3)] = TradingSbfError::Content, raised inside Trading at 33,088 CU before its first CPI.

The substituted readonly record is a well-formed ClaimsFoundingRequestV5 that differs from the honest one in exactly one coordinate — the founder whose Position and admission the founding mints — and is otherwise byte-identical, carrying the honest founding's own permit-intent digest. It is published as an ordinary content-addressed Registry record, so substituting the request is substituting an address. The outer's cross-request join is the only thing between that record and a Position minted to somebody else.

The refusal had to take Lock, Found, Realize, and Claims with it, because a chain that committed the Market and then refused would be worse than one that never ran. The transaction carries a prepended one-lamport System transfer, and the runner requires afterwards that the recipient does not exist, that the payer is debited by the fee and nothing else, and that all five program-allocated accounts are still vacant, System-owned, and empty.

The reordered-FundingState case is attributable now

W1e recorded this case as refusing for the wrong reason and refused to count it, which was the right call: in run 2 it consumed 527,965 CU against an out-of-memory death at 527,665, and in run 3, 561,607 against 561,101. Run 4 reproduced the coincidence at the new wall — 703,405 against 703,220 — because the honest transaction was still failing, just later.

Run 5 supplies the discriminator W1e named: the honest transaction succeeds with an identical frame shape, at 754,119 CU, and the hostile one refuses at 685,198 — 68,921 CU short of it, and 68,921 CU short of anywhere the honest path ends. The refusal is now attributable to the manifest binding it tests: a reordered tail derives an address the manifest entry at that position does not name, so the ordered-list digest cannot equal the artifact's funding_list_id.

The other bootstrap case remains sound and is unchanged in kind: DCLTPCB1 refuses a well-formed but non-terminal projected-Custody request at 22,860 CU, inside decode_projected_request, before any CPI. It cost 16,396 in run 3; the difference is the heap-frame instruction and the sysvar account.

Final Market state, and the three Claims accounts

Reacquired from the finalized chain after the founding, and checked field by field by the runner before the campaign was allowed to succeed:

accountownerbytescontents
MarketCore352phase Open, readiness Consumed, terminal receipt none, terminal winner 0, identity equal to the derived founding identity, rent beneficiary the founding generation's LifecycleRentCreditV2
Claims aggregateClaims288256 + 8×4, non-zero; the liability-basis market vector for a four-outcome Product
founder PositionClaims160128 + 8×4, non-zero
Claims admissionClaims512non-zero
Hoard vaultToken-2022165mint equal to the campaign's collateral mint, amount equal to the founding principal exactly, state Initialized
normal Custody replayCustody288was the 808-byte projection; realized in place, open_vault_count == 1, next_revision == 1, market and generation equal to the founded Market's
source vault, source replayclosed; both returned to the lifecycle credit by the Lock stage
one-shot permitconsumed by the commit-last Open stage; its lamports returned to the lifecycle credit

The Hoard's 165 bytes hash to the same digest as the source vault did before the founding, which is the arithmetic being visible: same mint, same Custody authority, and the same principal, moved.

Campaign transcript

Eighty-four transactions on one validator at cd05331. Six lines are hostile cases; each was required to fail and required to leave no poststate behind. Every witness in tools/gauntlet/tier1/witnesses.json passed (14 checked, 0 failed).

slot=35   cu=5778     wrong authority cannot initialize infrastructure          REFUSED
slot=67   cu=231335   initialize Core infrastructure profile
slot=99   cu=532863   immutable release activation refuses pre-revocation Core  REFUSED
slot=131  cu=2520     revoke Core Loader-v3 upgrade authority
slot=163  cu=543920   activate immutable release-set role: Core
slot=195  cu=565941   activate immutable release-set role: Claims
slot=227  cu=710601   activate immutable release-set role: Trading
slot=259  cu=260249   activate immutable release-set role: Resolution
slot=291  cu=226491   activate immutable release-set role: Custody
slot=323  cu=21478    late activation substitution rolls back prior transfer    REFUSED
slot=355  cu=5263     create real Token-2022 collateral and raw-atom wallet
...       ...         26 bounded Registry record and Product-graph publications
slot=451  cu=7686     publish record: substituted refund wallet refuses         REFUSED
slot=1251 cu=8621     create Market-scoped lifecycle RentCreditV2
slot=1283 cu=10661    create Found31 routing address lookup table
slot=1380 cu=6958     Found31 refuses substituted lifecycle credit              REFUSED
slot=1412 cu=141899   Found31 refuses a substituted Market coordinate and rolls back  REFUSED
slot=1444 cu=223540   create canonical Found31 Market
slot=1476 cu=3655     fund the founding principal supplier and its rent-capacity witness
slot=1508 cu=10121    create the founding generation's lifecycle RentCreditV2
...       ...         9 readonly request-record publications for the founding
slot=1957 cu=22860    DCLTPCB1 refuses a non-terminal projected-Custody request REFUSED
slot=1989 cu=685198   DCLTPCB1 refuses a reordered FundingState tail and rolls back   REFUSED
slot=2021 cu=754119   create the projected-Custody founding prestate (DCLTPCB1)
slot=2053 cu=900      pre-fund the founding's five program-allocated accounts
...       ...         12 record publications and 5 routing-table transactions
slot=2598 cu=33594    DCLTGMF1 refuses a substituted Claims request and rolls the whole founding back  REFUSED
slot=2630 cu=1184132  found the Market atomically: Lock, Found, Realize, Claims, Open (DCLTGMF1)

Artifacts

Built by the gauntlet from an isolated git archive cd05331 tree with the pinned toolchain (cargo-build-sbf 4.0.0, platform-tools v1.53, rustc 1.89.0), default release profile. Zero frame diagnostics on every program. These contain other lanes' concurrent work; only the Trading and runner changes in this section are this lane's.

ProgramBytesSHA-256
dclutch_registry_sbf.so220,7280033c6b55e8277dcd1c8f90ddcd100106b7c50d665758afee8af8a802c3a7058
dclutch_core_sbf.so1,007,09665803d559431e8bcd86276bad9a685bdc82c6b6ab90450625ba3bbe404952e75
dclutch_claims_sbf.so1,073,376ca3bcf4dafd353f157017ca4cd11a03e30445e1c68c7ce83b10090bef0a8d6cd
dclutch_trading_sbf.so1,349,992f977951484df61d7b74637efe87f9bdb3481c050408d84d6cb854f7607ada3dd
dclutch_resolution_proof_sbf.so463,57639a367ee6b60c771bf2c286557c5a6f01fcabd628d0f642292f19d363bf366ac
dclutch_custody_sbf.so347,53683eb5121559f1d41f75a9e47a4cdfd7cb8927236d8079ba42c8eee032b0195f9
dclutch_rent_sbf.so152,3123486a8197af492317a756e2fce659d399c5e32ff16323edac34fc1f1cafa7b8b

The frame, for whoever builds the next one

DCLTGMF1 is 135 + funding_count accounts, 138 for the demo Market's three-entry manifest, eight bytes of instruction data, ALT-routed as a v0 transaction over the same publish_routing_table machinery DCLTPCB1 uses.

What tranche-A family campaigns may now assume on chain

For the first time, something.

The census corroborates the routes rather than taking the campaign's word for them: fifteen blocked.json entries were deleted because their routes executed, including Claims FoundingV5, Core's generic founding with its Found-and-permit and Open stages, Custody's projected dispatch with Initialize, OpenHoard, OpenSourceCompartment, LockHoardAndCloseSource and RealizeAndClose, and Registry reauthentication.

Reproduced at a different revision, on different bytes

The campaign was run again at 67e441d, and this is a reproduction rather than a re-run: other lanes' work landed in between, so the Trading and Resolution ELFs are different bytes (6343ddf5247c37ec88387d8e54358b7b96bd2addea563901b347e6f5a56609b1 and 18dcc4a2a3375fe49c0aee3d332d7c7fa8b1cd69bb96606ca77c466facf89969; the other five are byte-identical). Eighty-four transactions again, and the whole gauntlet green: observe admits every binding, and 20 witnesses checked, 0 failed.

run 5 (cd05331)run 6 (67e441d)
DCLTPCB1, honest754,119774,639
DCLTPCB1 refuses a reordered FundingState tail685,198708,934
— margin below the honest transaction68,92165,705
DCLTPCB1 refuses a non-terminal request22,86022,161
DCLTGMF1, honest1,184,1321,189,823
DCLTGMF1 refuses a substituted Claims request33,59432,680
Found31223,540253,537

The census corroborates it rather than taking the campaign's word: 38 of 100 enumerated routes executed, up from 25 before this lane, and the two blocking entries still flagged stale are trading/hot_v3::process_hot_execution_v3 and trading/outer::process_activation#else — other lanes' entries, flagged for the census-enumeration limit named below and not because their routes ran. They were deliberately left alone.

Blocker H — a funded source compartment could not be unwound, ever

Found by asking what the census would say about the route this lane had just made reachable, and closed rather than queued.

OpenSourceCompartment puts real collateral under a projected authority against a Market that does not exist. No terminal accepted the phase it leaves behind. RefundAndClose admits HoardLocked; AbortOpenAndClose admits HoardOpen. That was deliberate, and its stated reason had the hazard exactly backwards — refusing the abort was called the safe direction because closing a funded projection would strand the principal:

once real principal is under this authority the authority over it cannot be destroyed, and the principal can only move forward through Lock.

The forward direction is the Lock stage of an atomic founding, and that founding's Core Found stage refuses at clock.slot > request.expiry_slot() (core-sbf/src/generic_founding_v1.rs:409) and its Open stage at current_slot > intent.expiry_slot() (:1594). So past expiry the principal could not move at all, in any direction, by any route. Not stranded rent — stranded collateral, permanently. W1d named it, chose it, and queued it; W1e did not touch it; this lane is the one that made the prestate routinely reachable, which is why it is the one that closed it.

AbortSourceAndClose (d43536d) admits SourceFunded and nothing else, after expiry, against a vacant Market. The principal returns to a token account owned by request.refund_owner — the party OpenSourceCompartment required to sign as the funder's owner — and the source Vault, the source replay, the empty Hoard Vault, and the projection all close to request.rent_credit. The RentCredit postcondition is exact, not a lower bound: those four are the complete set this ladder creates.

It is deliberately not an extension of AbortOpenAndClose, which was the tempting shortcut: that terminal admits HoardOpen holding nothing, Series drives it, its frame width is fixed, and widening it would put a principal transfer on a path whose whole contract is that there is no principal. A test pins that both pre-existing terminals still refuse SourceFunded, so the fix cannot later be "simplified" into the relaxation it was avoiding.

The request is derived, not supplied: it is the terminal Lock with exactly one field changed, at the same cursor, same amount, same refund owner. The same 768 bytes that authorise a founding authorise its unwind, and neither can name a coordinate the other does not. DCLTPCA1 carries only those bytes and — unlike DCLTPCB1 — does not require the founding artifact, because the persisted projection is already that binding and demanding the artifact would make reclaiming principal depend on the founder still holding a record they no longer need. It is also not on the extended-heap list: one CPI, one request, one sixteen-account frame.

Three defects surfaced by executing it, each of a kind worth naming:

  1. A list that was exhaustive by intent and not by the compiler. Custody decided whether the RentCredit may be writable with a matches! over three operations. The new terminal closes four accounts into it and was silently absent, so the route refused AccountFrame 5,364 CU in, with nothing in the logs naming a conjunct. It is an exhaustive match now (df81ce4): the next operation added to that enum is a compile error rather than a mystery.
  2. Ordering between manual lamport moves and CPIs. Every closure is individually balanced, but zeroing an account's lamports and assigning it to the System program by hand and then performing another CPI fails the runtime's own before-and-after check with UnbalancedInstruction, which names no account and no sum. The two older terminals never met it because each performs its single manual close last. This one does both token closures first and both manual closures after (fe9fced).
  3. A refusal that could not say what it was. The kernel has always distinguished Expiry from every other replay refusal; this program flattened both into Replay, so the pre-expiry refusal reported "replay PDA, owner, bytes, or revision refused" when the PDA, owner, bytes and revision were all correct and the transaction was merely early. CustodySbfError::Expiry is added and all three expiry-gated terminals map through it (d9f79bb). The census is what caught this: it refused to record coverage for a bound refusal that named no code, which is exactly how a real refusal launders itself out of a taxonomy.

Measured: the abort, on a validator

The campaign now stages two prestates. Both run the identical four-stage ladder; they differ in generation, in how long their founding stays satisfiable, and in which exit out of SourceFunded they take. The second exists to be abandoned. Run at d9f79bb, 100-transaction campaign, whole gauntlet green: 23 witnesses checked, 0 failed; 0 unclassified positions; 0 stale blocking entries.

transactionCU
stage the second prestate (DCLTPCB1)765,807
DCLTPCA1 refuses to abort before expiry134,666
DCLTPCA1 unwinds the expired compartment148,996

The refusal is the half that matters: while the founding is still satisfiable the authority over funded principal may not be destroyed, and an unwind that let anyone empty a live founding would be a worse defect than the stranding it was written to fix. It refuses with custody/CustodySbfError::Expiry, the runner requires the whole multi-instruction transaction to roll back to a fee-only debit, and it then re-authenticates the entire prestate to prove the refusal moved nothing. The honest abort, 419 slots later, returns exactly the principal to the party that supplied it, leaves all four staged accounts closed, and raises the lifecycle credit by exactly their four rents.

A margin that is closing, and belongs to nobody yet

DCLTGMF1 cost 1,184,132 CU at cd05331 and 1,278,747 at d9f79bb — 84.6% to 91.3% of the 1,400,000 per-transaction maximum, in one evening, from other lanes' concurrent changes to Core, Claims, and Trading. Nothing in this lane moved it and nothing is watching it.

There is no headroom to buy: the campaign already requests the maximum. At the current rate the atomic founding stops fitting, and it will stop fitting the way Found31 did — as a hard refusal at the ceiling with no partial result. A per-transaction CU budget for DCLTGMF1, checked in CI against a measured figure, is the thing that would turn that into a caught regression instead of a campaign that stops working. Naming it as an owner-decision because this lane found it by measurement and did not build it.

Owner-decisions this lane surfaced and did not take