Skip to content

fix(dlv): every authority-bearing field of the vault record is checked against the object that owns it - #759

Merged
cryptskii merged 1 commit into
mainfrom
fix/vault-record-metadata-authority
Sep 3, 2026
Merged

fix(dlv): every authority-bearing field of the vault record is checked against the object that owns it#759
cryptskii merged 1 commit into
mainfrom
fix/vault-record-metadata-authority

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

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.

Every rehydrated field that can change economic behavior, authority, settlement,
or verification must be derived from or checked against an authoritative
commitment before the record becomes usable.

What changed

field / surface authority it is now bound to change
baseline_state_ccb, baseline_presentation each other and this vault: presentation state_commitment must equal the immutable inner hash of the state bytes, decoded vault_id must be this vault's one verified accessor; the activation boolean and the publisher consume it instead of hashing raw row bytes
owner_genesis, owner_devid (composed path) the authenticated presentation's owner, via composition compose_own_vault refuses a record naming a different genesis or devid, by name
anchor_enforcement none — retired as authority AMM dlv.create refuses any posture but the canonical REQUIRED; 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 scheduled
put_amm_vault_record n/a — removed from shipping builds a pub insert-or-replace over every column with seven test callers and no production caller, compiled out
zero-owner write in dlv.create n/a — refused previously unwrap_or_default() on both identity fields; now refuses to create rather than persist a 32-zero owner

Why 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_digest is untouched. It is the DLV-policy commitment, not a CPTA anchor: the signed vault state already carries the CPTA pair separately as market_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.create including 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; three DlvSpecV1 constructions in tests/vault_funding_routes.rs now 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 sent OPTIONAL and now sends REQUIRED. The shipping AMM producer already sent REQUIRED.

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_enforcement row 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).

gate result
workspace (exact CI command, --release) 74 suites / 3946 passed / 0 failed / 17 ignored (dsm lib 1680/0, dsm_sdk lib 1807/0)
storage node 12 suites / 276 / 0
make lint (pinned 1.98.0) exit 0
ci/production_safety_checks.sh PASS
scripts/ci_scan.sh PASS

All on the exact stamped tree (status+diff checksum), re-run after the last edit.

…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.
@cryptskii
cryptskii merged commit 5e6530f into main Sep 3, 2026
18 checks passed
@cryptskii
cryptskii deleted the fix/vault-record-metadata-authority branch September 3, 2026 10:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant