feat(dlv): QuorumBind becomes the authoritative occupancy mechanism, and 2c-A freezes the bundle it binds - #778
Merged
Merged
Conversation
… by the successor its owner signed
C1 of the 5c-1 QuorumBind occupancy cutover. Pure core, additive: nothing
production-reachable changes yet. It exists so the switch commit has one
definition of "chosen" to read and one proof of close authority to check.
## The five-valued binding observation
`economic::cell_observation` answers this for a WRITE-ONCE cell and its
discipline is reused verbatim — the tally completes before anything is
selected, and an error is never evidence of emptiness. The tally itself is
not reusable. A write-once cell holds opaque bytes and "claimed" means a
quorum returned byte-identical contents; a binding key holds a record with a
round and a status, and chosen means q members hold an ACCEPTED record at
exactly the same round. Routing records through a byte-identical tally would
count a PROMISED record as a claim, so `dlv::binding_observation` states the
predicate directly and gets a fifth answer the old shape could not express:
Undetermined — at least q responses are attributable, but the evidence
establishes NEITHER a chosen value NOR a quorum of explicit absences.
That is not emptiness and not a forgery. Two quorums intersect, but one READ
need not see the intersection, so a value already chosen behind a down member
lands here. A composer reading it as "free" walks past a bind that is
mid-flight; a verifier reading it as a forgery makes every concurrent settle
permanently invalid.
The `Free`/`Undetermined` boundary is pinned by two tests differing in one
member, because getting it wrong in either direction is a real failure:
Accepted, Absent, Absent -> Free (q=2)
Accepted, Absent, Unavailable -> Undetermined
A quorum of explicit absences is decisive on its own. Any chosen value is
held by q members and every q-subset intersects them, so two members saying
"nothing here" prove nothing is chosen whatever the third holds. Treating a
stray minority record as blocking would let one lagging member freeze a
vault forever; the loser of that race is refused at bind time by the register
itself, and the observer does not have to serialize what the register already
serializes.
`QuorumBind::fold_reads` now folds through the same `tally_key`, so a
proposer and a composer cannot drift on what "chosen" means. No behaviour
change — the conformance suite, including both 4096-interleaving safety
tests, is unchanged and green.
## Closing a vault needs the owner, and QuorumBind cannot supply that
`x_close` is a public derivation. It says a generation is consumed by a close
rather than a trade; it says nothing about who did it. The binding register
is application-blind by design and never inspects the value, and `proposer_id`
is 32 self-asserted bytes — so on binding evidence alone, anyone could name a
victim's vault at its current c_n, bind a close-shaped bundle, and have every
composer fold that vault to zero reserves.
The write-once register supplied the missing half implicitly, by carrying an
owner-signed claim envelope, and the old walk checked it
(`claimant_public_key != owner.ak_pk`). Migrating occupancy to QuorumBind is
what turns that implicit dependency into a hole, so it is closed here, before
anything folds on binding evidence.
`dlv::close_authorization` reuses the operation DSM already signs.
`Operation::DlvClose` binds the whole transition — vault, both legs with
their amounts, the parent and terminal generation, the fee — and every field
is derived by the handler from the owner's verified frontier, never supplied.
A composer standing on that same frontier reconstructs the exact operation
and verifies the owner's signature over it; `bundle_signatures[0]` carries
only that signature.
Deliberately not a new commitment over `x_close` plus coordinates: a parallel
authorization would be a second canonical form of one object and the two
could disagree, and a signature over a close DISCRIMINATOR is not a signature
over the release successor, which is what actually moves the reserves. Six
mutations of the successor are pinned as refusals, so a genuine signature
cannot be lifted from one close onto another vault, generation, amount, leg
or fee.
## A close is alone in its bundle
`bundle_signatures[0]` authorizes one successor, and a mixed bundle has no
deterministic signature-to-transition mapping — a genuine owner close of V1
could ride beside a market leg on V2 that nothing authorized. Rather than
invent a mapping, `settlement_bundle::validate` removes the class: a bundle
is either one or more market transitions, or exactly one owner close. It is a
narrowing of the already-frozen 5a format, not a field change; every bundle
the tree constructs today already satisfies it, and enforcing it at the
canonical layer means a mixed bundle cannot be encoded, stored, fetched or
bound.
The close/market discriminator is therefore a bundle-level fact, and
`close_slot_commitment` moves to `settlement_bundle` where that layer needs
it. `settlement_slot_claim` keeps its old path as a re-export — one
definition, not a second — until 5d deletes that module whole.
## Verification (pinned 1.98.0)
- dsm: 2027 passed, 0 failed (24 suites, --release)
- quorum_bind_conformance: 10/10, both interleaving suites included
- make lint: exit 0
- ci/production_safety_checks.sh: exit 0 (all-features clippy + TLA+)
…stops reusing its own ballots C2 of the 5c-1 QuorumBind occupancy cutover. Additive: nothing production reachable calls any of this yet — the live sites move in C3, together with the frontier walk that reads them. The point of committing it separately is that every piece here is independently testable without a half-migrated tree. ## The reader that did not exist There was no client-side binding observer anywhere. The only learner logic in the repo was `QuorumBind::fold_reads` — private, and bound to a live transaction. So "move the read side to QuorumBind" was never a call-site swap; the reader had to be built. `binding_occupancy` is that reader, in two layers: - `observe_parent_key` reads `k_v = H(DSM/binding-keyset || c_n)` at every committed member and classifies it at the vault's committed q. Every member is asked rather than the first q, because a bundle that committed with exactly q holders needs the full fan-out to find them. - `observe_parent_binding` resolves the bound bundle and checks it against THIS parent. That second check is load-bearing, not defensive. The binding register is application-blind by design and never inspects the value it holds, so a proposer can put a bundle's record at a key the bundle does not name — and nothing in the read catches it. A test plants exactly that and asserts the refusal. The cross-storage-set refusal survives from the old walk for the same reason it existed there: a member reconfigured into another set keeps serving rows written under the old one. Because `k_v` derives from `c_n` alone, and `c_n` already commits the vault id, generation, reserves and pair, the old walk's separate "names a different cell" and "binds a different parent state" checks mostly collapse into *we read the right key*. What remains is what an application-blind register cannot enforce. **Nothing here returns a Result.** A transport failure IS `Unavailable`; an unresolvable bundle IS `Unresolvable`. An error channel beside the verdict is an invitation to collapse uncertainty into "the parent is free", which is the one reading that is never safe. Two tests pin that directly: members that cannot be reached, and a binding held below this reader's quorum, are both unresolvable rather than free. ## One attribution rule, one transport factory `read_binding_attributed` is the observer's half of the runner: same fan-out, same attribution, no engine and no writes. It shares `attribute_read` with `run` deliberately — an observer with its own attribution rule could count an answer the proposer would refuse. `binding_transport` replaces three separately-built transports with one cfg-split factory. That also un-pins `settlement_resume`'s live module from HTTP, so restart recovery is now exercisable under the deterministic double instead of being structurally untestable. ## A resumed bind no longer restarts at ballot zero `bind_settlement` hardcoded `base_ballot: 0`, while `settlement_resume` deliberately seeds from `fence.ballot`. Since `place_fence` is INSERT OR IGNORE, the persisted ballot survives an interruption — and the close-resume path re-derives the same deterministic bundle and calls straight back into `bind_settlement`, so this is a resume far more often than it looks. Restarting at zero reused ballots the transaction had already spent, which is exactly the recovery property the fence exists to provide. It now seeds from the fence. ## Test controls the driver cannot produce `plant_committed` writes canonical ACCEPTED records directly, because some states a hostile or degraded network reaches are unreachable through the driver: with n=3 and q=2, failing two members means no quorum forms at all, so the driver can never leave exactly one holder. Writing the record in is the honest way to reach that; the alternative is a test pretending the driver did something it structurally cannot. The double also gains member-id-keyed controls, resolved through the echo table it already keeps rather than a second source of truth. Tests name members the way the committed set does, so migrated call sites stay literal substitutions — and a hardcoded endpoint that matches no member is an injection that silently does nothing. `active_fence` exposes the row behind `active_verdict`, which becomes a two-line wrapper. The walk needs the tx_id as well as the verdict: a composer that finds its own close in flight over the parent it is standing on must be able to tell its own prepared close from somebody else's. ## Verification (pinned 1.98.0) - dsm_sdk lib: 1842 passed, 3 failed, all three the known half-migrated close (`dlv_close` writes the binding register while the walk still reads the old slot cell, so a closed generation composes as (1, 11000, 4547) instead of (2, 0, 0); and the resume pass looks for a FrozenClaimEnvelope the live close no longer writes). Both are what C3 closes, and neither is reachable from anything added here. - binding_occupancy 8/8, settlement_bind 2/2, quorum_bind_runner 5/5, trader_parent_fence 5/5, settlement_resume 2/2 - make lint: exit 0 - ci/production_safety_checks.sh: exit 0 (all-features clippy + TLA+)
…sm, and occupancy stops meaning composition
C3 of the 5c-1 cutover, and the commit the whole program was arranged around.
Every live settlement write, every economic reader, both route consumers, the
close path, restart recovery, provenance verification and the live-device proof
change authority together. Nothing is left believing the settlement-slot
register decides who owns a DLV parent.
## Why this could not be split
The two halves are each other's precondition. Move the writes without the walk
and every settled generation composes as free, which makes the vault
double-spendable. Move the walk without the writes and provenance rejects every
settle. There is no ordering of two commits where `main` is correct in between,
so this is one commit and its reviewability comes from the tests, not from size.
## The conflation, and the split
The old register could only be claimed by the party that settled, so "this
parent is consumed" and "this successor became economic state" were one fact.
They are not one fact:
binding occupancy = this parent is no longer available to a rival
realized frontier = this successor actually became economic state
For an owner close they still coincide — the successor is fully determined and
pre-authorized, so binding finality realizes it one-phase (Req 6.30). For a
market bundle they deliberately do not: a bound bundle whose trade has not
settled leaves the OLD reserves as the last realized state while the parent
stays blocked.
So `compose_vault_state` now has four outcomes where it had three, and the new
one carries real information:
free at quorum -> the chain ends here; this is the frontier
bound + realized -> fold and continue
bound + not settled -> the chain ends here, and the frontier is BOUND
anything else -> DLV_BINDING_EVIDENCE_UNAVAILABLE
That third row is why `a_claimed_generation_without_its_receipt_fails_closed`
is gone and `a_bound_generation_without_its_receipt_is_the_frontier_and_is_
marked_bound` replaces it. Under the old register a claimed slot with no receipt
was indistinguishable from a claimed slot with a wrong one, so both failed
closed — which took `dlv_reconcile`, the path that resolves exactly that
situation, down with the vault. The walk now reports both halves: nothing
realized, and the parent is occupied.
## Authorization before occupancy
`x_close` is a public derivation and the binding register is application-blind
by design (§22 #12), so anyone can build a close-shaped bundle naming a victim's
vault and bind it. The register accepts it; that is not its job. What stops a
composer folding that vault to zero is the owner's signature over the exact
release successor, which C1 added.
C3 is where the ORDERING is fixed, and the ordering is a separate property from
the signature. `dlv.close` signs with the device's CURRENT authority key while
every composer verifies under the authority the vault's PARENT committed. Those
can differ — a rotated or delegated owner authority — and then:
authorize, then bind -> refusal; nothing consumed; a retry is still possible
bind, then authorize -> the bind SUCCEEDS, the parent is consumed, and no
composer will ever realize it. The vault is
permanently occupied and unclosable, with no error
at the point of failure.
So `verify_close_authorization` is preflighted BEFORE the first mutating binding
op, and a signer that cannot satisfy the parent-committed authority is refused
before occupancy.
## Mutation controls (all three reproduce the forbidden STATE)
1. Close authorization removed from the walk -> a stranger's bound close-shaped
bundle folds the victim's vault to `generation: 1, reserve_a: 0,
reserve_b: 0`. Named test red: `a_stranger_cannot_close_a_vault_by_binding_a_
close_shaped_bundle`.
2. `Undetermined` mapped to `Free` -> a parent whose bind is in flight composes
as a live quotable frontier. Named test red: `a_key_with_no_chosen_value_is_
neither_a_frontier_nor_an_edge`.
3. Preflight moved after the bind -> `generation 0 is bound by SFT5Q0ZW... but
its close is not authorized by the vault owner`: occupancy consumed by a
close that can never realize. Named test red: `a_close_this_device_cannot_
authorize_leaves_the_parent_untouched`.
Control 3 is worth reading twice. On its first run it stayed GREEN across all 34
close tests, because no fixture could construct a signer that fails the parent's
authority — the preflight's POSITION was unobservable and the gate only looked
covered. The router-level test above exists because of that, and it is what
turns a green mutation into a real control.
## The live hardware proof moves with the mechanism
`scripts/dlv_market_rig_proof.sh` read the settlement-slot endpoint and tallied
digests across nodes against a hardcoded quorum. Porting that tally to the
binding register in shell would have put "chosen at a key" in two languages, and
the copy with no `BindingRecord` type would decide whether a live proof passes.
`dlv_binding_probe` instead composes the vault as any third party does and asks
the PRODUCTION observer, emitting `key=value` lines the script asserts. Its
public surface is one function and one struct; the rest of `binding_occupancy`
stays `pub(crate)`, because a tooling requirement is not a reason to publish
composition internals. Exit status separates "could not ask" from "answer was
unwelcome" — the script decides which verdicts are acceptable.
## Restart recovery has exactly one driver
`settlement_resume::recover_all` had ZERO callers while `dlv.close` already told
the user an unresolved close "stays fenced and recovery will resume it". It is
now wired into `storage.sync`, after the client DB, identity, catalog and
transport are up and BEFORE the close-intent pass that finalizes what it
resolves. `resume_close_intents` no longer re-drives a bind; a fence that cannot
be resolved stays intact and retries, and is never turned into "free".
## Consumers
Four gates, all turning "a quote that is guaranteed to lose at bind time" into
an early refusal: the router drops candidates whose parent is not free; pointer
publication reuses the existing `HopParentNotCurrent`; the market settle accepts
its OWN bound bundle (same X, a retry) and rejects anyone else's; and
`finish_prepared_close` requires the fold to be an `OwnerClose` whose bundle
identity matches the fence this device actually drove, so a market settle at the
same generation cannot be mistaken for the resumed close.
## Deliberately unchanged
The market realization gate — receipt, published RouteCommit, curve
re-simulation — is kept VERBATIM as 5c-1 scaffolding and marked for wholesale
deletion in 5c-2, which replaces it with the accepted trader successor plus
`A_B`. `settlement_slot_claim` survives because `dsm_storage_node` imports its
verifier; that excision is one coordinated cut in 5d.
## Verification (pinned 1.98.0, all three gates on one unchanged tree)
- workspace board (`--workspace --exclude dsm_storage_node --release`):
**4063 passed, 0 failed**, 76 suites
- dsm_sdk lib 1848/0; dsm 2027/0; composition 16/16; binding_occupancy 8/8;
quorum_bind_conformance 10/10; economic_dlv_settle_provenance 16/16
- `make lint` — exit 0
- `ci/production_safety_checks.sh` — exit 0 (all-features clippy + TLA+)
- `dlv_binding_probe` builds and its argument handling is exercised
## Not in this commit
- **5c-2** replaces the temporary market realization gate with the exact bundled
trader successor accepted under ordinary DSM plus `A_B` verified, and extends
the rig gate with that realization proof.
- **5d** removes the storage-node settlement-slot endpoint, tables and proto
leftovers as one coordinated cut, together with the four `pub` transport
functions that dead-code lints cannot flag.
- **5e** is the beta wipe and redeploy.
The branch stays unmerged until 5c-2 and the old-register excision complete.
…ng src/bin The C3 rig port added `dsm_sdk/src/bin/dlv_binding_probe.rs` — the probe `scripts/dlv_market_rig_proof.sh` now calls to prove binding occupancy on live hardware. It never entered the repository. `dsm_sdk/.gitignore` is an ANDROID ignore file (`*.apk`, `*.dex`, `*.class`, `.gradle/`) that was copied into a Rust crate, and its unanchored `bin/` — meant for Android build output at the crate root — also matches Rust's `src/bin/`. The failure mode is what makes this worth a commit of its own rather than a quiet `git add -f`: the file was invisible. `git status` reported a CLEAN tree, `git add -A` skipped it without comment, and the commit's own `--stat` showed no new file. Everything local kept working, because cargo compiles what is on disk and does not care what git tracks — the board built the probe and `make lint` even reported errors in it. Only a fresh checkout would have found the rig gate calling a binary whose source does not exist. Anchoring to `/bin/` keeps the pattern doing its job for Android output at the crate root while leaving `src/bin/` alone. `gen/` and `out/` are left unanchored: both plausibly appear nested in build output, and neither collides with a Rust source convention. No source changed, so the C3 stamps stand: 4063 passed / 0 failed across 76 suites, `make lint` 0, `ci/production_safety_checks.sh` 0.
… sync was re-proving it
C3 wired `settlement_resume::recover_all` into `storage.sync`. Its work list came
from `list_unresolved_fences`, which selected BOTH `fenced` and
`committed_awaiting_acceptance` — so every sync re-drove a full QuorumBind round
for every settlement this device had ever successfully completed: re-committing
a decision already made, burning a ballot, and re-fetching the immutable bundle,
once per settle, forever. Unbounded and monotonically growing.
The defect is conceptual, not arithmetic. Two different things were being called
"unresolved":
Fenced the QuorumBind outcome is unresolved
-> a recovery worker must re-drive the binding
CommittedAwaitingAcceptance QuorumBind already COMMITTED. Terminal here.
Only ordinary-DSM acceptance of the exact
permitted successor remains — which QuorumBind
cannot supply and must not be asked for.
So the selector is narrowed to `WHERE state = 'fenced'` and renamed
`list_binding_recovery_fences`, because the old name is what made the conflation
easy to write. `active_fence` is deliberately unchanged and still returns both
states: a committed fence is no longer recovery work, but it is still an
advancement constraint (`PermitsOnly(exact_successor)`) and it still supplies the
close-resume identity check. Nothing is released here.
That leaves the post-COMMIT interval without an owner, which is correct and
deliberate: acceptance is 5c-2's job, and it needs a continuation path that
finishes the exact permitted successor, completes economic admission, constructs
and verifies the trader-acceptance artifact, and only then releases the fence.
A binding driver cannot do any of that, which is why it should never have been
handed the work.
Two controls, because narrowing a work list can also just empty it: a committed
fence is absent from the recovery list while still reporting
`PermitsOnly` and still being discoverable through `active_fence`; and a `Fenced`
row is still returned.
Also lands the canonical plan documents this branch has been working from
(`docs/plans/` is gitignored; kept ones are force-added), including the 5c-1 plan
that never got its repo copy.
## Verification (pinned 1.98.0)
- workspace board: 4065 passed, 0 failed, 76 suites
- trader_parent_fence 7/7, quorum_bind_runner 5/5, settlement_resume 2/2
- make lint exit 0
- ci/production_safety_checks.sh exit 0 (all-features clippy + TLA+)
…n 2c-B
Amendment 2c-A freezes the CCB objects that `b` is computed over. Def 6.14's
flat twelve-member tuple becomes two nested objects: `0x000E SettlementBundle`
carries only `market_terms?` and `transitions`, and everything only a market
settlement has moves into a new nested `0x0033 MarketTerms`. An owner close
carries none of it, so it fabricates no dummy intent, no empty route and no
zero route-set commitment — which is what the shipped path does today.
`0x000F ConsumedDlvTransition` carries the parent by digest and the successor
as the complete nested `V_{n+1}`, because the successor is fully specified
before binding but is not yet authoritative realized state. Reserve deltas are
dropped: two authenticated endpoints already determine the movement, and the
route states the quantities once. `0x0010 DlvProofMaterial` ships with zero
fields rather than a manufactured witness record.
`storage_set_id` and `q` leave the bundle. The authenticated parent `V_n` is
the authority for both, and trusting a proposed successor's own quorum would
let a proposer choose it.
The boundary this amendment does NOT cross, stated because it is easy to
overclaim: 2c-A fixes `b`'s derivation and all outer canonical structure, and
the owner-close shape is fully constructible today. `MarketTerms` field 6
`recovery_material` is mandatory and its nested class is 2c-B's, so a
conforming market `b` is intentionally not constructible until 2c-B lands. No
encoder follows from this document alone.
Registry: adds 5.19-5.22, allocates `0x0033` from a namespace audit rather
than by counting the table (the table is behind the code by sixteen numbers,
which is 2c-C's to absorb), and corrects two stale schema statements that
would have frozen a burned nesting on first ship - `VaultStateV2` ships
schema 4 with 1/2/3 burned, `StorageSet` ships schema 3 with 1/2 burned, both
since f00d1e0. `StorageSet`'s field table now declares `enc(entry)` for its
tuple element, which 2.4 does not cover, and the member-id-only sort rule the
encoder actually implements.
Plans: marks 5c-1 and the C3 amendments historical where 2c-A supersedes their
wire shape, and corrects nine passages in 5c-2 that would have instructed an
implementer in the deleted shape. Two of those are behavioural: the fence
release is emitted only after TA_B verifies, never beside AdvanceOutcome, and
removing the spurious owner-close trader-parent fence - which Req 6.30 says
should not exist - is assigned to the 5c-2 commit that consumes the new shape.
Also records the owner-close size gate. A pinned owner-close bundle is 50,330
bytes, almost all of it one SPX256f signature, and it clears every configured
path with 10.4x margin against the binding 512 KiB device_auth body buffer.
The trace exposed a separate defect at the durability seam: put_bundle
discards the PutImmutable fan-out result, so zero accepting members still
returns Ok and binds a value_addr no member holds.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
The 5c-1 cutover that makes QuorumBind the one authoritative occupancy mechanism, plus the 2c-A
amendment that freezes the canonical bundle it binds.
The governing correction: binding occupancy and realized composition are different facts. The
settlement-slot register fused them, because with a write-once register "consumed" and "composed"
coincided. Under QuorumBind they must not.
Commits
89c3e5fc— a binding read has five answers, not four.Undeterminedis the new one: atleast
qresponses are attributable, but the evidence establishes neither a chosen value nor aquorum of explicit absences. Mapping it to
Freewould make a mid-flight bind look unbound;mapping it to
Invalidwould make every concurrent settle a forgery. Also: a close is authorizedby the successor its owner actually signed, reusing the canonical
Operation::DlvCloseratherthan inventing a second commitment.
989e787c— the occupancy question gets an answer (binding_occupancy.rs), and a resumedbind stops reusing its own ballots.
9461cf9f— the switch, atomic by necessity: walk, market write path, close resume, the coretrait across both impls, the provenance arm, eight consumers, restart recovery, and the rig gate,
all moving together. Splitting it would create a window where either every settled generation
composes as free, or provenance rejects every settle.
87fda1fc— an Android.gitignorein a Rust crate was silently untrackingsrc/bin.ea3c16fb— a committed binding is not binding-recovery work; every sync was re-driving afull Paxos round for every settlement ever completed.
31184fb5— amendment 2c-A, the registry edits, and corrections to the two plan documents.2c-A
Def 6.14's flat twelve-member tuple becomes two nested objects.
0x000E SettlementBundlecarriesonly
market_terms?andtransitions; everything only a market settlement has moves into a newnested
0x0033 MarketTerms. An owner close carries none of it, so it fabricates no dummy intent,no empty route, no zero route-set commitment.
0x000Fcarries the parent by digest and the successor as the complete nestedV_{n+1}— thesuccessor is fully specified before binding but is not yet authoritative realized state. Reserve
deltas are dropped; two authenticated endpoints already determine the movement.
The boundary, stated because it is easy to overclaim: 2c-A fixes
b's derivation and all outercanonical structure, and the owner-close shape is fully constructible.
MarketTermsfield 6recovery_materialis mandatory and its nested class is 2c-B's, so a conforming marketbisintentionally not constructible until 2c-B lands. No encoder follows from this alone.
Registry corrections worth naming:
VaultStateV2ships schema 4 with 1/2/3 burned andStorageSetships schema 3 with 1/2 burned, both since
f00d1e0c— freezing0x000Fagainst the documentedschema 3 would have shipped a burned nesting on day one.
StorageSet's table now declaresenc(entry)for its tuple element, which §2.4 does not cover.Verification
The board was run at the
ea3c16fbtree.31184fb5is documentation-only and touches no code, sothat result stands unchanged.
Mutation controls, each reproducing the forbidden state rather than merely breaking liveness: close
authorization dropped from the walk (a non-owner folds a funded vault to zero),
Undeterminedmapped to
Free(the walk composes past a mid-flight bind), and the mixed-bundle ban removed (agenuine owner-signed close of one vault folds a second vault it never authorized). The
close-authorization mutation initially stayed green because no fixture could construct a signer
failing the parent's authority; a test was added, and it then went red with the exact forbidden
state.
Deliberately not in this PR
The market realization gate is still the pre-bundle evidence path, marked in code for wholesale
deletion. 5c-2 replaces it with
TA_B; 5d excises the settlement-slot register and the nodeendpoint. The branch is unmerged-by-design until those land — both mechanisms are live in the tree
today by construction, not by oversight.
One defect found while sizing the owner-close bundle and not fixed here:
put_bundlediscards thePutImmutable fan-out result, so zero accepting members still returns
Okand binds avalue_addrno member holds. The fix is
accepted >= qbefore any occupancy mutation, on its own branch.