Skip to content

fix(dlv): the owner proves the settlement from the state it owns before it signs or folds - #761

Merged
cryptskii merged 1 commit into
mainfrom
fix/owner-fold-proves-curve-and-parent-binding
Sep 3, 2026
Merged

fix(dlv): the owner proves the settlement from the state it owns before it signs or folds#761
cryptskii merged 1 commit into
mainfrom
fix/owner-fold-proves-curve-and-parent-binding

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

Summary

Settlement blocker one: the owner proves the exact transition from the state it owns before it signs or folds a settlement. Cut from merged main (dbd1a56e, #760); no dependency on the source-map branch.

authoritative parent vault state (the head's own vault-state leaf at parent_sequence)
        ↓
committed pair + committed fee            (must reproduce that leaf over the reserves consumed)
        ↓
authoritative reserve_in / reserve_out    (by ASSET IDENTITY: the leaf of the asset the trader paid)
        ↓
canonical constant_product_output(input)  (the one implementation; no duplicate math)
        ↓
must equal the proposed output, exactly
        ↓
signed parent_binding == commitment of the parent state these leaves hold
        ↓
only then may the owner sign (route) / fold (core)

What was wrong

dlv.reconcile copied input_amount/output_amount from the trader's receipt into the signed DlvOwnerApplyV2 and the core ApplySettlement arm moved the leaves by checked add/sub. The receipt witnesses what the trader committed, not what the owner's reserves pay. Composition re-simulates the route-commit hop on the way to the frontier (vault_state_composition.rs) and the 0x0026 verifier re-simulates the trader's settle (provenance.rs), but nothing on the owner's side ever ran the curve on the amounts the fold actually moved — and advance never read the signed parent_binding (every reference was a test literal). A receipt above the curve folded.

The cut

layer change
core ApplySettlement arm (advance) — the security boundary, sufficient alone (1) the head's own vault-state leaf at parent_sequence must equal compute_vault_smt_value(parent, reserves_digest(pair, fee, cur_a, cur_b)) — the fee and reserves the curve runs on are the committed ones; no leaf ⇒ refused, never synthesised. (2) parent_binding (signed) must be vault_state_commitment(parent_state), and that state's vault, generation, both reserves, pair, fee and owner must equal the leaves — the head holds no V_n, so CCB(V_n) rides the mutation as unsigned bytes tied by the signature. (3) reserve_in is the leaf of the asset the trader paid; constant_product_output(input, reserve_in, reserve_out, fee) must equal the proposed output exactly. Typed refusal each; nothing moves.
VaultReserveMutation::ApplySettlement gains parent_state: Vec<u8>
dlv.reconcilepre-sign mirror, early refusal, not authority pair, fee, reserves and parent_binding come from the verified composition (which now retains each folded parent's state, so reconciling N generations back names the exact state that trade consumed); the SQLite-record fee read is deleted; same orientation rule, same canonical function, exact equality — refused before sign_operation_sphincs
composition folded_parent_bindings: Vec<(u64, c_n)>folded_parents: Vec<FoldedParent { generation, c_n, state }>
DeviceState::parent_vault_state_for_tests #[cfg(any(test, feature = "testing"))] — describes the parent state the head itself holds, so fixtures name the real state they consume

CPTA enforcement is untouched at its three layers (route pre-flight, advance funnel, economic verifier). Not in this PR: R_econ admission for settle/owner-apply, ReleasePolicy semantics, close/apply redesign.

Dependent sweep — a finding, not a footnote

Every owner-apply fixture in the tree folded an off-curve literal: 100 for 90 on 10 000/5 000 (curve: 49); 1 000 for 970 (curve: 453); 100 for 60 on 500/400 (curve: 7); 100 for 60 on 100/100 (curve: 9); a foreign receipt claiming 500 where the curve pays 453 — and the consumed-generation route test reconciled that receipt as its winner. Each is re-rooted through the canonical function, with the known values pinned beside the derivation. That the whole suite was green on those numbers is the measure of the gap.

Proof

check result
core: one unit above OR below the curve refused, root unchanged; exact output advances green
core: priced by the input asset's own reserve (b → a); the a → b price refused for that direction green
core: a fee the leaf never committed refused by the head's own commitment, before the curve green
core: a parent_binding that is not the supplied bytes' commitment refused; one committing a well-formed state the leaves do not hold refused; the real one advances green
route: a receipt one unit off the curve refused before signing (route-named refusal), root, both leaves and consume-once row untouched; the curve-priced receipt folds and consumes the generation green
mutation, per arm (core leaf check, core binding check, core curve check, route pre-sign check) each arm disabled by an inverse edit that keeps the gate called and discards its verdict: core curve equality → the above/below test red; core leaf check → the fee test red; core binding-equals-commitment → the parent-binding test red (case a); core state-equals-leaves → the parent-binding test red (case b); route pre-sign → the route test red on the route-named message, the fold caught by core instead. Restoration shasum -c OK each time; green after
re-rooted suites: core device_state, dlv_owner_apply_preservation, dlv_value_op_signing, vault_rehydration, funded_creation_tests green
boards workspace 74 binaries, 3956 passed / 0 failed / 17 ignored (dsm lib 1686/0, dsm_sdk lib 1811/0/7 ignored), exit 0; node 11 binaries, 276/0, exit 0; root make lint exit 0; ci/production_safety_checks.sh PASS (pinned 1.98.0, incl. TLA+); scripts/ci_scan.sh PASS — all on tree stamp 9788e485d2c7, identical before and after every run and after the mutation cycles

…re it signs or folds

Settlement blocker one. The owner fold (dlv.reconcile -> DlvOwnerApplyV2 ->
the ApplySettlement arm of advance) took input_amount and output_amount
verbatim from the trader's receipt. The receipt is the trader's witness of
what the trader committed; it says nothing about what the owner's reserves
pay. Composition re-simulates the route-commit HOP on the way to the
frontier, and the 0x0026 verifier re-simulates the trader's settle, but no
owner-side code ever ran the curve on the amounts the fold actually moved,
and advance never read the signed parent_binding at all. A trader could
commit a receipt above the curve and the owner's own reserves would fold it.

The core ApplySettlement arm is now the security boundary and is sufficient
by itself. From the head's own commitment outward:

    vault-state leaf at parent_sequence  (commits pair, fee, both reserves)
      -> the mutation's pair + fee over the reserves this arm consumes must
         reproduce that leaf; no leaf => nothing to consume, never synthesised
      -> parent_binding (signed) == vault_state_commitment(parent_state), and
         that state's vault, generation, reserves, pair, fee and owner equal
         the leaves — the head holds no V_n, only the leaves V_n commits, so
         CCB(V_n) rides the mutation as unsigned bytes tied by the signature
      -> reserve_in is the leaf of the asset the trader PAID, by asset
         identity, never a caller's "a"/"b" declaration
      -> the ONE canonical constant_product_output over those reserves and
         that fee must equal the proposed output exactly; no band
      -> only then do the leaves move.

VaultReserveMutation::ApplySettlement gains parent_state: Vec<u8>.

dlv.reconcile mirrors the same derivation BEFORE signing — an early refusal,
not an authority: pair, fee, reserves and parent_binding now come from the
verified composition (composition retains each folded parent's state, so an
LP reconciling N generations back names the exact state that trade consumed),
never from the SQLite record and never from the receipt. The record read
for the fee is gone. The owner does not sign arithmetic it has not checked.

Dependent sweep: every owner-apply fixture in the tree folded an off-curve
literal (100 for 90 on 10 000/5 000; 1 000 for 970; 100 for 60 on 500/400;
100 for 60 on 100/100; a foreign receipt for 500 where the curve pays 453).
Each is re-rooted through the canonical function and names the head's own
parent state via a testing-gated DeviceState::parent_vault_state_for_tests.

Proven, each by a named test and an inverse-edit mutation cycle:
core — one unit above OR below the curve refused, the exact output advances;
pricing by the input asset's own reserve (b -> a), the pair-order price
refused for that direction; a fee the leaf never committed refused; a
parent_binding that is not the supplied state's commitment refused, and one
that commits a well-formed state the leaves do not hold refused; route — a
receipt one unit off the curve refused before signing with the root, both
leaves and the consume-once row untouched, and the curve-priced receipt
folds. CPTA enforcement is untouched at all three layers. Not in this cut:
R_econ admission for settle/owner-apply, ReleasePolicy semantics, close.

Boards: workspace 74 binaries, 3956 passed / 0 failed / 17 ignored (dsm lib 1686/0, dsm_sdk lib 1811/0/7 ignored), exit 0; node 11 binaries, 276/0, exit 0; make lint exit 0;
production safety PASS (pinned 1.98.0); ci_scan PASS.
@cryptskii
cryptskii merged commit 39588c6 into main Sep 3, 2026
27 of 28 checks passed
@cryptskii
cryptskii deleted the fix/owner-fold-proves-curve-and-parent-binding branch September 3, 2026 19:09
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