Fractional twin V3: selected-coordinate physics and bounded retirement
Date: 2026-08-28 Status: executable Claims/Trading child rung; not a live capability
Decision
Fractional keeps its own terms-owned K shard model. It does not lower through Rational's receipt recipe.
FractionalExposureTermsV2 is the sole durable owner of:
- the ordered
Kshard Mint identities; - the exact integer denominator
D > 1; - the Product-
Nto Claims-Kexposure identity; - the Product and representation bases.
divide_exposure_shards_v2 remains the sole quotient/remainder boundary:
whole = shard_atoms / D
consumed = whole * D
change = shard_atoms - consumed
The division refuses a zero input, a sub-denominator input, an invalid coordinate, and checked-integer overflow. change remains in the same source Token account under the same Mint. There is no rounding credit, remainder ledger, or second supply projection.
Why the Rational child ABI is refused here
Rational's descriptor requires a receipt Mint and a K-wide coefficient vector. Generic Fractional V2 terms contain neither fact. They instead name K independent shard instruments and one exposure bundle. Supplying an arbitrary coefficient vector or making every coefficient equal to D would create a second Structured receipt recipe with no Fractional semantic owner. It would also create receipt/custody resources that the Fractional model does not need.
That is the divergence audit required by decision 0011's Option-A rule. The result is a refusal, not a hand-filled descriptor.
Selected-coordinate topology
plan_fractional_physical_v3 binds the exact V2 request to authenticated terms and produces this closed topology:
| Action | Exact quantity boundary | Child route | Outer authority |
|---|---|---|---|
| Wrap | mint D * whole; no remainder | one Claims call atomically mutates native Claims and invokes Token-2022 | holder |
| Transfer | raw same-Mint atoms; no division | ordinary Token-2022 TransferChecked | source holder |
| WholeUnwrap | burn consumed, keep change, return whole native Claims | one Claims + Token-2022 atomic call | holder |
| TerminalRedeem | same division; positive payout remains Claims terminal-evaluator-owned | one Claims + Token-2022 + Custody atomic call | holder |
| TerminalZeroBurn | same division; payout is exactly zero | one Claims + Token-2022 atomic call | holder |
| Terminalize | no Token effect | Fractional state only | permissionless |
| V2 ZeroSupplyRetire | refused | use ordered V3 retirement | permissionless |
This module does not accept a caller-supplied payout and does not reproduce the denominator arithmetic. Transfer intentionally bypasses the family caller: it changes only Token-owned balances.
Durable retirement split
The V2 retirement planner expands all K Mint closures into one route. That is not a viable runtime-width topology: an ALT can reduce packet bytes but cannot raise devnet's 64-account lock limit.
V3 adds a 296-byte exact cursor and a 288-byte exact request with three actions:
Beginbinds release, Market, terms, Token program/behavior, exposure, producer root, RentCredit, width, cursor rent principal, and the next root revision.RetireCoordinateaccepts onlycoordinate == next_coordinate, rebinds all identities to the immutable terms, authenticates the exact terms-owned Mint and Claims reserve, requires both supply and reserve balance to be zero, and advances the cursor and revision by exactly one.Finishhas noK-account tail. It is available only atnext_coordinate == Kand produces fixed-width terminal evidence before the cursor/root and their historical rent principal are closed.
Replay, skip, Mint substitution, nonzero supply, nonzero reserve, missing Mint or Claims authentication, stale revision, reserved-byte mutation, and terms/exposure substitution all refuse in the hostile corpus.
The cursor is a progress fact only. Token-2022 remains the owner of Mint supply, Claims remains the owner of native reserve balances, and Lifecycle Rent remains the owner of rent refund accounting.
Exact candidate frame census
topology_v3.rs freezes the maximum-distinct account geometry for the missing caller. The fixture gives every permitted role a distinct key, compiles a single-instruction v0 message against an activated-table shape, includes the required 64-byte signature, and asserts exact wire bytes. These are frame contract numbers, not real-ELF execution evidence:
| Transaction | Unique locks | Instruction data | Fully signed v0 bytes | Packet margin |
|---|---|---|---|---|
| Wrap / WholeUnwrap | 31 | 417 | 682 | 550 |
| Direct TransferChecked | 5 | 10 | 222 | 1,010 |
| TerminalRedeem / TerminalZeroBurn | 44 | 417 | 708 | 524 |
| Terminalize | 18 | 417 | 656 | 576 |
| Retirement Begin | 8 | 289 | 508 | 724 |
| Retirement Coordinate | 22 | 289 | 536 | 696 |
| Retirement Finish | 10 | 289 | 512 | 720 |
Every transaction is below 64 unique locks and 1,232 bytes. At K = 256, the retirement sequence is one Begin, 256 identical bounded Coordinate transactions, and one fixed Finish. Width changes transaction count, never a transaction frame. This is the architectural fix; the ALT is only packet compression.
Real SBF frame measurement
The Claims link was freshly rebuilt from this worktree with the repository's measurement flags and then read with tools/sbf-frame-sizes.py:
RUSTC_BOOTSTRAP=1 RUSTFLAGS="-Zemit-stack-sizes --emit=obj,link" \
CARGO_TARGET_DIR=<fresh> cargo build-sbf \
--manifest-path programs/dclutch-claims-sbf/Cargo.toml -- --locked
The build emitted zero SBF stack-overwrite diagnostics. The measurement object SHA-256 was 0172fc58454973ff637f9a91feb0f07ecfaba5af78e71cb42cab57aefeda0c85. It contained 201 measured frames, none at or over 4,096 bytes. The relevant Fractional frames were:
| Function | Static frame | Margin |
|---|---|---|
| open Claims + Token child | 3,904 | 192 |
| boxed terminal Claims/Custody child | 3,200 | 896 |
| terminal Fractional handler | 2,880 | 1,216 |
| terminal receipt emission | 1,600 | 2,496 |
The public Claims selector is split into bounded no-inline routing frames. The split changes neither instruction bytes nor family order; it prevents adding the Fractional family from causing the otherwise-green legacy selector to be recompiled as one 4,672-byte frame.
What is implemented
- exact physical action planning with denominator conservation;
- a production Claims handler for the exact 31-account Wrap/WholeUnwrap frame;
- a distinct production Claims handler for the exact 44-account terminal frame, deriving its terminal Claims request from authenticated chain state;
- native Claims mutation followed by Token-2022 mint/burn, and terminal Claims/Custody settlement followed by Token-2022 burn, so a late child or postcondition refusal aborts the enclosing SVM instruction;
- exact 256-byte open and terminal receipts with family-request, Claims, Custody, Token poststate, root, quantity, and payout bindings;
- Trading composition admission for all four executable actions, with distinct receipt kinds and two authenticated
invoke_signedauthorities: the exact request caller PDA and the terms/Market Fractional-root PDA; - caller-side Claims instruction builders for the 31-account open and 44-account terminal frames that rederive both PDAs, selected record pairs, canonical Claims Positions, selected Mint/Token accounts, terminal exposure bytes, and every privilege;
- explicit refusal of the old all-
Kretirement route; - exact hostile-decoded retirement request and cursor formats;
- strict ordered cursor transitions and fixed-width finish evidence;
- maximum-distinct lock and signed-v0 packet fixtures;
- adversarial conservation, replay, skip, substitution, zero-state, authentication, revision, and encoding tests.
What still blocks a live Fractional capability
The native Claims/Token/Custody child and its Trading composition arm now exist. Integration still requires:
- generated EffectProgram, AccountProfile, RequestProfile, and ExecutionStrategy artifacts whose exact expanded frame matches the census;
- the outer Trading transition that advances the sole Fractional-root revision only after the verified child receipt, plus its rollback campaign;
- a migration or new producer-root version that authenticates the V3 cursor PDA and its lifecycle rent;
- one-coordinate Claims Position close plus Token Mint close in the retirement step, and fixed final Core/Lifecycle-Rent closure;
- a real-ELF caller-backed late-Token-failure rollback campaign, 20-seed CU mean, and checked release bindings.
Until those exist, the current 14-action release remains refused and this code must not be described as a complete executable or deployed Fractional market.
Superseded 2026-08-29 by FRACTIONAL_LIFECYCLE_2026_08_29.md
The section below records the first execution and the width wall as they stood mid-morning. Both have moved since, so read the lifecycle doc instead:
* the 256/258 wall was ruled deliberate and closed — the campaign compiles its own geometry rather than widening the terms or the shared fixture; * all four published actions, including both terminal ones, now execute and commit; * the refusal blamed on width below turned out to be a shipped defect that made terminal settlement dead for every representation, fixed in 4953bada; * the operative width bound is 64, not 256 and not the arithmetic maximum 98.
Update 2026-08-29: first execution, and the width wall it found
programs/dclutch-claims-sbf/program-test/fractional-atomic/ now executes the production fractional_atomic_v3 handler against real ELFs. The route needed a caller able to sign the Trading caller-authority and the Fractional root in one invoke_signed; test-programs/fractional-atomic-caller/ is that caller.
Executed: the transaction enters the real Claims ELF at CPI depth two and consumes 37,281 compute units inside the Fractional route before refusing. The exact 31-account frame, both invoke_signed authorities, the four finalized record pairs, the activated release set, the decoded root, and the Token-2022 Mint controller all authenticate against real bytes.
Not executed, and blocked on a decision rather than on work:
fractional_exposure_terms_bytes_v2refuses a representation width above 256, so a Fractional capability names at most 256 shard Mints;validate_commonrequiresmarket.claim_count == terms.representation_width();- the shared Claims LBV2 fixture, and the runtime width this evidence profile is built around, is 258.
So no Fractional capability can bind a Market wider than 256 outcomes, and the 258-outcome fixture cannot carry one at all. The wrap therefore refuses with ClaimsSbfError::Economic. Resolving it means either raising the terms ceiling or bounding Fractional Markets at 256 and giving the campaign its own narrower Product/LBV2 fixture. Until then Fractional Wrap has no committed on-chain effect, and no rollback, transfer, or terminal Claims/Custody/Token campaign exists.