fix(dlv): every authority-bearing field of the vault record is checked against the object that owns it - #759
Merged
Conversation
…d against the object that owns it The vault record is a local SQLite row. After #758 its pair, fee, reserves and generation were checked against the vault-state leaf, but every other field it carries was still trusted because the row existed. This binds each remaining field to the object that actually owns that authority — never by enlarging the vault-state leaf, which stays a local-coherence mechanism. - The two baseline blobs are bound before anything consumes them. One verified accessor requires the presentation's state_commitment to equal the immutable inner hash of the state bytes (pairing the blobs to each other) and the decoded state's vault_id to be this vault's. The FUNDED->MARKET-ACTIVE activation boolean and the routing publisher both consume it; before, the activation hashed whatever bytes the row held after an emptiness check. Two byte-equalities, stated as such: it authenticates nothing on its own — composition's P0-P6 verification still does that — it stops a stale, desynced or cross-pasted row from reaching activation. - The composed owner is compared to the record. compose_own_vault gets its owner from the AUTHENTICATED presentation and never compared it to the row; now a record naming a different genesis or devid is refused by name. The comparison is to the verified composition, not to another local copy; rehydration already checks the row against the head. - anchor_enforcement is retired as authority. The code that decides whether a hop's vault-state binding is accepted, enforce_parent_binding, is unconditional and never consulted it; the selector could only ever have described a weaker posture than the one in force, and its intended gate was already dead. An AMM create now refuses any posture but the canonical REQUIRED one (scoped to the AMM branch — the only shape that persists the field; non-AMM and posted DLVs never carried it). No decision reads the persisted value again: the rehydrated posture is a derived constant, the range-check refusal is gone with it. The column and the LimboVault field survive as deprecation residue, labelled dead in code with the schema removal scheduled. Zero is never defaulted into behaviour again. - put_amm_vault_record, a pub INSERT OR REPLACE over every column with seven callers and none in production, is compiled out of shipping builds. Production inserts through the transaction-scoped closure that refuses a second creation. - dlv.create refuses instead of persisting a 32-zero owner identity when there is no device head (previously unwrap_or_default on both fields). Defensive today — get_current_state() errors earlier on such a device — so the fabrication cannot return if that ordering changes. Dependent sweep, done this time: three DlvSpecV1 constructions in tests/vault_funding_routes.rs carried the default posture and would have turned the board red; a positive control there asserted only the absence of two substrings and would have been silently voided rather than reddened; the Android SoFi harness sent OPTIONAL. All fixed. The shipping AMM producer already sent REQUIRED. policy_digest is deliberately UNTOUCHED: it is the DLV-policy commitment, not a CPTA anchor, and its provenance is a separate cut. Each new gate mutation-proven per arm with inverse-edit restoration. Boards: workspace 74 suites / 3946 passed / 0 failed / 17 ignored (dsm 1680/0, dsm_sdk 1807/0); node 12 suites / 276 / 0; make lint exit 0; production safety PASS (pinned 1.98.0); ci_scan PASS.
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.
Summary
The metadata-authority cut. After #758, the vault record's pair, fee, reserves and generation were checked against the vault-state leaf; every other field was still trusted because the row existed. This binds each remaining authority-bearing field to the object that owns that authority — never by enlarging the vault-state leaf, which stays a local-coherence mechanism.
What changed
baseline_state_ccb,baseline_presentationstate_commitmentmust equal the immutable inner hash of the state bytes, decodedvault_idmust be this vault'sowner_genesis,owner_devid(composed path)compose_own_vaultrefuses a record naming a different genesis or devid, by nameanchor_enforcementdlv.createrefuses any posture but the canonicalREQUIRED; no decision reads the persisted value; the rehydrated posture is a derived constant; column and field survive as residue labelled dead in code, schema removal scheduledput_amm_vault_recordpubinsert-or-replace over every column with seven test callers and no production caller, compiled outdlv.createunwrap_or_default()on both identity fields; now refuses to create rather than persist a 32-zero ownerWhy enforcement is retired rather than committed.
enforce_parent_binding, the code that decides whether a hop's vault-state binding is accepted, is unconditional and never consulted the selector; its intended tri-state gate was dead code that said so of itself. A selector whose gate is dead can only ever describe a weaker posture than the one in force, and minting a new signed commitment to preserve it would ossify dead policy. Behaviour on the security path is unchanged; what changed is that the row can no longer be read back as authority.Stated limits. The baseline accessor is two byte-equalities and authenticates nothing on its own — composition's P0-P6 verification still does that; what it buys is that a stale, desynced or cross-pasted row cannot reach activation. The publisher's consumption of it is a refactor, not an independent gate, and its comment says so.
Not in this PR, deliberately
policy_digestis untouched. It is the DLV-policy commitment, not a CPTA anchor: the signed vault state already carries the CPTA pair separately asmarket_policy. Its provenance — the DLV-policy layer's derivation and its binding into the signed birth authority — is the next cut, and its schema label is corrected there. A first attempt at this PR added a two-copy check for it and was rejected in review: it made the frozen vault post load-bearing for rehydration while the record commits inside the advance transaction and the post is frozen after it, which would have let a crash strand a funded vault its owner could never close.Dependent sweep
The first attempt was rejected for placing the create-posture refusal before AMM branching, which refused every
dlv.createincluding the shipping frontend's non-AMM and posted DLVs, and would have turned the board red through three integration sites. Fixed and swept: the refusal is scoped to the AMM branch, the only shape that persists the field; threeDlvSpecV1constructions intests/vault_funding_routes.rsnow carry the canonical posture; a positive control there that asserted only the absence of two substrings, and would have been silently voided rather than reddened, now reaches the gate it exists to prove; the Android SoFi harness sentOPTIONALand now sendsREQUIRED. The shipping AMM producer already sentREQUIRED.Proof
Named tests with a positive control beside each refusal: each baseline blob mutated, two valid blobs cross-pasted between vaults, a foreign vault id in the decoded state, the composed-owner mismatch, the AMM create-posture refusal, and an
anchor_enforcementrow mutation proving it can no longer weaken the posture. Each gate mutation-proven per arm with inverse-edit restoration and checksums. Test count reconciled by name: three new tests in the vault routes, one replaced in rehydration (the unknown-enforcement refusal, whose subject no longer exists), the rest unchanged.Reviewed on three lenses in the first round (rejected, four blocking) and two in the correction round (approved, zero blocking, every previous blocker resolved).
--release)make lint(pinned 1.98.0)ci/production_safety_checks.shscripts/ci_scan.shAll on the exact stamped tree (status+diff checksum), re-run after the last edit.