Skip to content

S21: escrow vaults, released by the canonical verdict on an external commitment - #1114

Merged
cryptskii merged 14 commits into
mainfrom
feat/escrow-vaults
Oct 5, 2026
Merged

cryptskii merged 14 commits into
mainfrom
feat/escrow-vaults

Conversation

@cryptskii

@cryptskii cryptskii commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Draft until CI is read and the Code map is repinned from this head's map. Specification approved by the owner 2026-10-05; the implementation is complete on this branch.

Why

An application needed two parties to lock equal stakes against one agreed match, with the application as referee deciding who takes both. Nothing in DSM could hold value under a condition other than a SoFi market (CONFORMANCE §6.74). Owner rulings, quoted in SoFi Amendment S21:

  • 2026-10-04: a generic escrow vault, and no wager or battle logic in Core.
  • 2026-10-05: one external commitment has one admissible verdict, enforced by the protocol rather than by the referee's bookkeeping.

What it does (SoFi §19.9)

  • The vault. A SoFi vault whose three policy slots name one EscrowTerms object (class 0x0063): a token, Y = H(DSM/external/v1 ∥ X), and 1–16 branches (an outcome, the exact 1–4 signers that decide it, a recipient). VaultStateLeaf is unchanged. Created by operation 38 EscrowVaultCreate, which debits exactly the stake.
  • The verdict cell. K_verdict = H(DSM/escrow/verdict-cell/v1; Y ∥ τ), with τ the outcome table's digest. Its value is the first EscrowVerdict (class 0x0064) at the leader that proves its own authority from its own bytes: the key recomputes, its signers are exactly the outcome's, and every SPHINCS+ signature verifies over the statement for this cell. A second verdict never becomes the cell's value.
  • Release. A new settlement branch (classes 0x0065/0x0066): the branch recipient's own position, paying the whole stake and retiring the vault. It realizes only on the cell's verdict final on its outcome; on another outcome it is Void and its key is skipped (arm (v)). So linked vaults settle on one outcome.
  • No market, no owner close. Close and Swap need a market; a Release needs escrow terms.
  • Routes. escrow.party, .create (locks against a counterpart only if it derives the same cell), .sign, .adjudicate, .verdict, .release, .locked, .vaults, with proto messages and envelope payloads 129–133.

Found on the way (a SoFi defect, fixed here)

A setup made right after a trader's SoFi position names the claim that position's resolution accepted. Peers read it through validate_peer_lineage, whose target step refuses a conditional claim. So after a trader traded or released through one vault and then set up with another, nobody else could walk the second vault past that trader's exercise. peer_lineage::peer_claim_at resolves a conditional target through the S15 resolver, and accepted_claim_at reads through it (MR-SOFI-0347). The escrow node test in which the winner releases two vaults failed without it.

Evidence

  • Node tests (dsm_sdk::handlers::escrow_e2e_tests, two players and a referee on the pinned set's nodes):
    • the winner takes both stakes once, and the loser's release of the winner's branch is refused by Core past its producer;
    • a referee who signs both outcomes settles both vaults on the first, and a release built on the second is Void;
    • a verdict signed outside the authority, or made for another cell, is passed over;
    • a joint cancel and a referee result cannot both settle;
    • an escrow vault has no market and no owner close, and a stake is not locked against a vault bound to another cell.
  • Core unit tests for the wire (frozen goldens), recognition, gathering, validation, resolution, genesis acceptance, the creation write set and conservation.
  • Mutation controls: 18 gates removed one at a time in a scratch worktree, each turning its named test red (VERIFICATION_MATRIX.md, the S21 rows). The kind check (Close/Swap/Release by terms) is the type and has no mutation.
  • CONFORMANCE: MR-SOFI-0363–0386 Met; totals regenerated; static evidence gate 0 failures.
  • Local: make lint exit 0; real-code guard clean; targeted tests green after each merge of main (A11: a Web2 application connects to a wallet (DSM Connect), proven with a game on three phones #1112, fix: a device's held first admission resumes on its activation root #1113).

Owed before ready

  • CI's board on this head, read in full.
  • Code map repin from this head's CI map (new symbols, and three manifest rows renamed market_legs_permitted → vault_tokens_permitted).

…l verdict

An application needed two parties to lock equal stakes against one
agreed match, with the application as referee, and nothing in DSM could
hold value under a condition other than a SoFi market. The owner ruled
(2026-10-04) for a generic escrow vault and no wager logic in Core, and
(2026-10-05) that one external commitment has one admissible verdict,
made so by the protocol rather than by the referee's bookkeeping.

SoFi section 19.9 specifies it on the vault machinery:
- an escrow vault is a vault whose three policy slots name one
  EscrowTerms object (class 0x0063): a token, Y = H(DSM/external/v1 || X)
  and 1 to 16 branches, each an outcome, the exact set of signers that
  decides it, and a recipient;
- it releases its whole amount once, by the recipient's own Release
  position (classes 0x0065 and 0x0066), and has no market and no owner
  close;
- the verdict occupies K_verdict = H(DSM/escrow/verdict-cell/v1; Y || tau)
  first at its leader and proves its own authority from its bytes
  (EscrowVerdict, class 0x0064); tau is the digest of the outcome table,
  so only the agreed signers can occupy the cell;
- every vault bound to the cell settles only on the outcome it holds:
  ConsumedRoute requires the verdict final on the release's outcome, and
  RouteImpossible arm (v) skips a release that lost it, which resolves
  Void;
- creation is operation variant 38, EscrowVaultCreate.

MR-SOFI-0363 to 0384 are added with source amendment, all Missing;
CONFORMANCE section 6.72 records the finding; section 1 is re-pinned and
the totals regenerated. The implementation follows after the owner's
review of the amendment.
Review of S21 asked that linking be an explicit invariant, so that two
parties cannot believe their vaults share a verdict when they do not.
Section 19.9 now states it:
- the outcome table has exactly one encoding, so the same outcomes with
  the same signer sets give the same tau, and the table is the verdict
  authority (there is no other);
- two vaults are linked exactly when they name the same Y and
  byte-identical tables, and then they share K_verdict; a vault that
  differs in any byte is bound to another cell and is not linked;
- a link is derived from each vault's accepted terms, never asserted by a
  match id, an index entry or an application, and is checked by whoever
  relies on it: escrow.create against a counterpart, and any reader
  waiting on both stakes;
- discovery is by cell, so escrow_commitment_locator(Y) becomes
  escrow_cell_locator(K_verdict), and a vault bound to another cell is
  never found among the linked ones;
- Core accepts each genesis on its own terms and compares no two vaults;
  the section says why, and what protects the party that locks first.

MR-SOFI-0365, 0373 and 0384 rewritten; MR-SOFI-0385 and 0386 added,
Missing; section 1 re-pinned and the totals regenerated.
The wire layer of escrow vaults (SoFi section 19.9), and nothing that
spends yet:
- CCB classes 0x0063 to 0x0066: EscrowTerms, EscrowVerdict, and the
  Release branch and its route digest, allocated for the next commit.
- Nine domain tags in common/domain_tags/dsm/misc/escrow.rs:
  DSM/external/v1 and the eight DSM/escrow/* domains; the registry count
  goes from 353 to 362.
- EscrowTerms, EscrowOutcome, OutcomeTable, EscrowBranch, EscrowSigner,
  EscrowVerdict and VerdictSignature, with strict codecs. A signer set is
  1 to 4 signers strictly ascending by their canonical bytes, and an
  outcome table 1 to 16 entries strictly ascending by outcome, so a
  duplicate, a threshold or a second encoding of one table cannot be
  expressed.
- sofi::escrow: Y, A_T, tau, K_verdict, the verdict seed, the statement
  m(o), the two locators, signing a statement, and verdict_authority:
  a verdict occupies its cell only when its Y and table derive the cell,
  its outcome is in the table, its signers are exactly that outcome's,
  and every signature verifies over m(o). The verdict cell is read with
  route_chain::evaluate; the read keeps why each value ahead of the
  occupant counts as nothing, and standing_for gives a release Final,
  Unsettled or Lost.

Tests (dsm --lib, release): 8 in sofi::escrow, covering the derivations
against an independent hasher, the field-table bytes with frozen
digests, linked vaults sharing one cell exactly when Y and the table
agree, every refusal of a malformed table, and a signature deciding only
the cell it was made for. Domain tag registry 8/8. clippy clean.
…on (SoFi S21)

Core of escrow vaults, per SoFi section 19.9.

Release:
- B° gains the Release branch (class 0x0065) and its route digest
  (0x0066): one leg, E in its single-leg form, naming the vault, the
  verdict cell and the outcome; no verdict enters E.
- VaultTerms resolves a vault's slots to a market's policies or, when all
  three name one object, to its escrow terms (authenticated under the
  terms namespace). Close and Swap require a market, so against an escrow
  vault they are Invalid (TermsAreNotAMarket); a Release requires escrow
  terms (TermsAreNotEscrow).
- validate_release: one leg; the vault's own derivation, pinned set,
  Active; the whole stake (reserve_a, reserve_b = 0); the cell the terms
  derive; a branch for the outcome that pays the trader; the closed write
  set with the vault retired; the realize root.
- market_legs_permitted becomes vault_tokens_permitted and checks the
  escrow vault's one token too; close_vault_post becomes
  retire_vault_post.

The verdict in resolution:
- RouteFacts, GroundFacts and EstablishedFacts carry a VerdictFact.
  ConsumedRoute needs the verdict final on the release's outcome;
  RouteImpossible gains arm (v), VerdictOnAnotherOutcome, which skips the
  key without validation evidence; rung 5 voids a release that lost.
- The verifier reads a Release's verdict cell beside its legs, keeps its
  completion proof when final, and reports an undecided cell as
  NotEstablished::VerdictCell.
- An acquisition fetches a token policy the predicate named missing on
  the next round, which is how an escrow vault's token is fetched.

Creation:
- Operation variant 38, EscrowVaultCreate {genesis_preimage, creation,
  terms, signature}, signed over the operation like SofiVaultCreate;
  egress, ClosedWriteSet, and verified in DeviceState::advance.
- Its write set: one debit of the stake of the terms' token and the
  creation record, insert-only; the terms must be the object all three
  slots name. Conservation holds the deltas to that one debit.
- GenesisAccepted has its escrow form (GenesisTerms::Escrow);
  vaults_of_token passes over escrow vaults, and vaults_of_cell finds the
  vaults bound to a verdict cell under escrow_cell_locator.
- Publication: the terms, the escrow genesis (indexed by vault and by
  cell), and gathered verdicts (by statement).

History: TX_TYPE_ESCROW_LOCK and TX_TYPE_ESCROW_RELEASE, the SDK's
Realized kinds and the wallet's labels; the frontend proto regenerated.

Tests (release): dsm --lib sofi/economic/types 619 passed, then the new
escrow tests: 9 release validation, 3 ladder, 5 write set, 4 genesis
acceptance, 2 publication, 1 conservation; sofi_v8_operations covers tag
38's signature arms and round trip. dsm_sdk wallet_routes 26/26.
Frontend mapper and wallet tests 35/35, tsc clean. clippy -D warnings
clean on dsm and dsm_sdk; the real-code guard clean;
conformance_evidence clean. CONFORMANCE section renumbered 6.74 (6.72
and 6.73 are taken by #1112 and #1113).
…e (SoFi S21)

escrow_flow runs each escrow route over the vault machinery of sofi_flow:

- create publishes the terms and the genesis Stored, checks a named
  counterpart vault is accepted, Active and bound to the same verdict cell,
  then admits the creation with its one debit;
- sign puts this device's signature for an outcome under the cell's
  statement locator; adjudicate gathers the outcome's signatures there,
  assembles the verdict Core recognizes, writes it to the cell leader first
  and returns what the cell holds, which may be an earlier verdict;
- release builds only on a final verdict whose branch pays this device,
  sets up with the vault, walks it to its head, retires it and exercises
  the release through Core;
- locked and vaults list escrow vaults by cell and by this device's own
  creations.

Core gains gathered_signatures and assemble_verdict, with their test.
sofi_flow's VaultAtHead carries the vault's terms, so market routes take a
market and escrow routes an escrow vault, and the helpers the flow shares
are crate-visible. sofi_sdk gains build_escrow_vault_create and
draft_release.
Conflicts:
- dsm/src/common/domain_tags/mod.rs: EXPECTED_TAG_COUNT 368 = 353 at the
  merge base + A11's 6 + S21's 9; both notes kept.
- MASTER_REQUIREMENTS §1: the pins re-taken from the merged spec bytes (DSM
  explainer = main's, SoFi = this branch's); the pin history S21, then A11.
  §7.1 holds both entries, A11 then escrow. Counts: DSM 25, SoFi 55,
  storage 30, 973 canonical rows.
- CONFORMANCE_GAPS: main's §6.72, then this branch's §6.74 (§6.73 is #1113).
  §7 totals regenerated by ci/conformance_evidence.py --write.
…d peers read it so

A setup made right after a trader's SoFi position names the claim that
position's resolution accepted. A peer validating the setup read the
trader's claim there through validate_peer_lineage, whose target step
refuses a conditional claim as Unresolved: so once a trader traded (or
released) through one vault and then set up with another, nobody else
could walk the second vault past that trader's exercise.

peer_claim_at walks the segment as a payer's and takes the position
itself as its last step, resolving a conditional claim through the S15
resolver exactly as an interior position, and accepted_claim_at reads
another trader's claim through it (MR-SOFI-0347). Found by the escrow
node test in which the winner releases two vaults in turn.
… S21)

escrow.party, .create, .sign, .adjudicate, .verdict, .release, .locked and
.vaults reach escrow_flow through handlers/escrow_routes, with their
requests and responses in dsm_app.proto (envelope payloads 129-133, after
Connect's 128) and refused as requests by the Core bridge.

Five node tests run them on the pinned set's nodes with two players and a
referee: the winner takes both stakes once, and the loser's release of the
winner's branch is refused by Core past its producer; a referee who signs
both outcomes settles both vaults on the first, and a release built on the
second is Void; a verdict signed outside the authority, or made for
another cell, is passed over; a joint cancel and a referee result cannot
both settle; an escrow vault has no market and no owner close, and a
stake is not locked against a vault bound to another cell.

escrow.release splits into its producer's pre-check and
exercise_release, which drafts and exercises through Core. sofi_flow
prices a named vault only once it has a market, so an escrow vault is
refused for that and not for its token.
… conditional-claim finding

CONFORMANCE §6.74: S21 is implemented; MR-SOFI-0363 to 0386 are Met on
their code and their unit and node tests, and the finding found on the
way (a peer could not read a setup's claim at a conditional position) is
recorded with MR-SOFI-0347's row, which gains peer_claim_at. §7 totals
regenerated.

VERIFICATION_MATRIX: ten escrow rows. Every gate was removed in a scratch
worktree and its named test watched red: the release checks, the
verdict's cell, signers and signatures, gathering, the ConsumedRoute
conjunct, rung 5, arm (v), the release's standing at the cell, genesis
acceptance, the creation write set and conservation, the counterpart's
cell, and the conditional-claim arm. The Close/Swap/Release-by-kind
refusal is the type and has no mutation.
…vaults

CONFORMANCE: main's §6.73 (#1113) goes before this branch's §6.74; §7
totals regenerated from the merged rows (ci/conformance_evidence.py,
static: 0 failures). The verification matrix merged cleanly.

Checked on the merged tree: dsm economic_admission_lifecycle 18/0;
dsm_sdk escrow_e2e_tests 5/0 and faucet_flow_tests 13/0; clippy clean;
real-code guard clean.
…w fn has a caller

CodeQL alert 750 (rust/cleartext-logging): the loser's-release test
Debug-printed the PositionOutcome of a release signed with the device
key. The panic now says only what happened.

Rust gates (G1, ci/sofi_reachability.py) found five pub fns in
sofi/escrow.rs with no production caller:
- verdict_object_address duplicated Publication::address for a gathered
  verdict, and check_verdict_completion had no consumer at all: both
  deleted, with the MR-SOFI-0369 row's citation of the second.
- sign_statement, gathered_signatures and assemble_verdict are called by
  escrow_flow, which imported the module as `escrow::{self, ..}`; it now
  imports `dsm::sofi::escrow` as a module of its own, the form G1 resolves.

Local: ci/production_safety_checks.sh passes (G1: 128 reachable, the
baseline unchanged); clippy and the real-code guard clean; conformance
evidence static clean.
… wait) into feat/escrow-vaults

Clean: main brings INTENT_PINS.tsv (the 97 rows #1113 moved, re-pinned on
main's tree) and a test-only wait in crates/dsm-app-host/tests/
real_connection.rs. This branch's own re-pin follows from this head's CI
map.
Code map run 37294345826 (head ce886f7) read 0 failing rows and 622
failing pins: 616 PIN_STALE, all code-class; 3 ORPHAN_PIN and 3 UNPINNED,
the MR-SOFI-0304/0311/0318 rows renamed market_legs_permitted ->
vault_tokens_permitted.

- 616 repinned (0 moved beyond their code; 324 evidence tests);
- the three old keys unpinned and the three renamed rows pinned.

Board logs, all of ce886f7: CI's Rust tests (dsm) 1658 ok,
workspace-rest 262 ok, Storage Node (Postgres) 101 ok; and 50 tests those
logs do not attest, run targeted on a clean tree at ce886f7: 47 dsm_sdk
(not yet reported by CI) and dsm::economic_lineage_register's two, whose
CI log interleaves the next binary's header before their result line.
All passed. make requirement-map-intent: 0 failing rows, 0 failing pins,
635 pinned.
@cryptskii
cryptskii marked this pull request as ready for review October 5, 2026 12:13
@cryptskii
cryptskii merged commit a5e9226 into main Oct 5, 2026
27 checks passed
cryptskii added a commit that referenced this pull request Oct 5, 2026
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.

2 participants