Repository navigation
S21: escrow vaults, released by the canonical verdict on an external commitment - #1114
Merged
Merged
Conversation
…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
marked this pull request as ready for review
October 5, 2026 12:13
cryptskii
added a commit
that referenced
this pull request
Oct 5, 2026
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.
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:
What it does (SoFi §19.9)
EscrowTermsobject (class0x0063): a token,Y = H(DSM/external/v1 ∥ X), and 1–16 branches (an outcome, the exact 1–4 signers that decide it, a recipient).VaultStateLeafis unchanged. Created by operation 38EscrowVaultCreate, which debits exactly the stake.K_verdict = H(DSM/escrow/verdict-cell/v1; Y ∥ τ), withτthe outcome table's digest. Its value is the firstEscrowVerdict(class0x0064) 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.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.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_atresolves a conditional target through the S15 resolver, andaccepted_claim_atreads through it (MR-SOFI-0347). The escrow node test in which the winner releases two vaults failed without it.Evidence
dsm_sdk::handlers::escrow_e2e_tests, two players and a referee on the pinned set's nodes):VERIFICATION_MATRIX.md, the S21 rows). The kind check (Close/Swap/Release by terms) is the type and has no mutation.make lintexit 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
market_legs_permitted→vault_tokens_permitted).