Repository navigation
The Escrow tab in SoFi; the wallet's decisions moved into Rust (app lock, pair order, token fields, burn units); biometric removed - #1117
Merged
Conversation
…upply escrow.lock: the wallet names each outcome's signers and payee as this device or a contact; Rust resolves their keys and genesis, orders the signers and branches canonically, and refuses a stranger, a repeated signer or a repeated outcome. escrow.locked / escrow.vaults report each branch with whether this device decides it and whether it pays this device. App lock: the PIN or pattern is enrolled in Rust as an Argon2id hash and checked there. Each miss is stored before it is answered; after the third only the wallet's recovery phrase opens it (compared in constant time with the seed the wallet holds). No clock. session.unlock no longer opens the session unchecked, a locked wallet cannot change its lock, the lock settings are stored by Rust when configured, and the preferences route refuses every key the lock keeps. Biometric unlock removed: the Game Boy shell offers none. Its proto kinds are reserved and the Android arm is gone. sofi.create_vault takes the two tokens in either order and orders them itself; token.create takes the supply as typed and scales it in Rust.
The frontend no longer hashes, stores or checks the PIN or pattern, and
keeps no attempt count or cooldown. session.unlock answers every try with
Rust's session snapshot: still locked with the tries left, or only the
recovery phrase opens it; the lock screen renders that and offers the
phrase field once Rust requires it. session.configure_lock enrolls the
PIN or pattern in Rust and takes the method alone ("none" turns the lock
off); Rust decides what a lock is (a PIN of 4 to 8 digits, or 8 presses of
the shell's buttons) and stores the settings itself.
The auto-lock-after-inactivity picker is gone: it stored a timeout that
nothing ever read. The settings copy says what the lock does.
The frontend test drives the real lock screen over the bridge, answered
from Rust's own record of the lock (ingress.rs), which must equal the live
answers.
Escrow tab (Trade → SoFi, beside Swap and Liquidity): lock a stake of one token, its outcomes each naming who decides it and who it pays (this device or a contact); list this device's escrow vaults with each outcome's role for this device; sign or decide an outcome this device decides; check the verdict; release a stake an outcome pays this device; find the vaults bound to a verdict cell. The copy explains mechanics only. Rust names every party's keys, orders the terms and answers every state. token.check answers what token.create would refuse, field by field, and creates nothing; the creation wizard shows Rust's reasons and keeps no rules of its own. A ticker is 2 to 8 characters, counted as characters. The wizard now shows Rust's refusal message on a failed create (it read a field createToken never set). token.burn takes the amount as typed and parses it at the token's committed decimals: the frontend read it as base units, so burning "5" of a two-decimal token burned 0.05 while the screen said 5. The frontend tests drive the real wizard and the real escrow tab over the bridge, answered from Rust's own records (ingress.rs): token.check on a local device, and escrow on a funded device on the network's nodes. The record helper answers a request Rust answered more than once in Rust's order.
…h the ingress The unit test exercised the refusing helper, not the route: a route that stopped calling it stayed green. The test now asks prefs.get and prefs.set for every key the lock keeps as the WebView asks them.
Rust's refusals reach the frontend as thrown errors, and so does a dropped bridge, so createToken reported both as a failed creation and the wizard's ask-again path for an ambiguous outcome never ran: a token created while the call was lost was shown as not created. The ingress boundary now throws a RustRefusal for Rust's answer; createToken returns that as a creation that did not succeed and rethrows anything else, so the wizard asks again with the identical request. Tested over the bridge against Rust's own record of a refused token.create (the token wizard's record gains it); each of the two mutations of the distinction turns one of the two tests red.
The lock covered the screen only: with the session locked the app could still call every route, read balances and accept a bilateral transfer. While the session is locked the ingress now refuses every route and envelope but the session's own and the receiving loop the host keeps going, so value still arrives; the JNI calls the WebView reaches directly (balances, bilateral accept, reject and cancel) refuse too. Once the lock opens, the lock screen has the wallet and the contacts read again. The lock is on exactly while a PIN or pattern is enrolled: the separate "enabled" setting, the path for a device whose lock the old frontend set, and its test are gone, and the method is stored with the credential. The proto reservations and removal notes this branch added are gone too. Recorded: CONFORMANCE_GAPS §6.75 (S-LOCK resolved; MR-SOFI-0303 and MR-SOFI-0349 rows), and VERIFICATION_MATRIX rows for every gate of the branch with its mutation: fifteen in Rust and two in the frontend, each red, each restored.
…rded Run on a Samsung A16 on the beta fleet: the lock end to end (PIN, misses, phrase, refusals while locked, restart, cold start) and an escrow locked, decided and released on one phone (CONFORMANCE_GAPS §6.75).
cryptskii
marked this pull request as ready for review
October 5, 2026 15:28
…r outcome With three actions in one row the labels ran off the buttons on the A16. The two-phone escrow run is recorded in CONFORMANCE_GAPS §6.75.
What it was granted, its account, the requests handled and what this wallet did for it open from the card's header. The approval screen still shows the grant in full; it is where it is read.
CodeQL (rust/cleartext-logging, 3 high) flagged the assertion messages, which named the refused secret; they now name the refusal expected.
The SDK's node harness runs the five pinned nodes on one Postgres server, and each node's pool could open db::POOL_MAX_SIZE = 32 connections: 160 against the 100 CI's postgres:16 service admits. Under the bursts #1111's HTTP/2 and parallel reads produce, Postgres refused connections ("sorry, too many clients already"), the node answered /bytecommit/mirror and /bytecommit/proof with 500, and a reader held fewer verified links than the cell had. A claim final at every seat then read as "not final yet". That is the failure one_resolution_asks_each_node_for_a_final_cell_or_an_ object_once has shown in 7 CI runs since #1111 (B's sofi.route: "vault owner lineage: position N: the claim holding the root cell is not final yet", N = 1, 3, 4, 5, 6), and token_routes_admit_create_and_burn_end_to_end once on the writer's own read ("the claim holds the cell, Preserved"). Established by execution: - When create_vault returns, A holds no pending admission, and every one of A's root cells (positions 1-8) reads Final with links at all five seats, to A's settlement read and to B's sequential and concurrent reads. The SDK does not return before the owner's claim is final, and the test's ordering is already the one it depends on. - On a private server with max_connections = 300 the test passed 40+ runs (alone, the whole node_e2e module, and under CPU stress). At 100 the Postgres log shows the refusals and the 500s land on the evidence requests; at 50 even the boot fails. - Production is unaffected: each deployed node has a sibling Postgres of its own (deploy/docker-compose.node.yml), which grants all 32. The fix: - db::create_pool takes the pool's size from its caller. The node binary and the node's own tests pass POOL_MAX_SIZE, unchanged. - NodeSet::start gives each node an equal share of what the server admits beyond other clients' connections (max_connections less the reserved slots), capped at POOL_MAX_SIZE: 19 per node at 100. The harness's admin pools hold one connection. - test_support::nodes_tests::the_shared_server_grants_every_connection_a_ node_set_may_hold holds every connection each pool may hold at once, and checks no pool opens one past them. It sits beside nodes.rs, not in it: dsm-app-host's real-connection suite mounts nodes.rs by #[path] (and gets the same sized pools). Pools of 32 fail it on the refused connection; a pool built at 32 and then resize()d fails it too, since deadpool's resize on an empty pool forgets no permits and the pool keeps opening connections past its size.
Evidence: CI's board at bb517e1 (dsm, dsm_sdk, workspace-rest, storage node job logs, 3,097 tests ok); the two storage-node binaries the job log interleaved re-run locally at the same commit. requirement-map-intent over CI's map: 0 failing rows, 0 failing pins.
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.
What this does
The Escrow tab (Trade → SoFi, beside Swap and Liquidity). A party can lock a stake of one token whose outcomes each name who decides them and who they pay (this device or a contact), see their escrow vaults with each outcome's role for this device, sign or decide an outcome they decide, check the verdict, release a stake an outcome pays them, and find the vaults bound to a verdict cell. The copy explains mechanics only: no opponent, referee or wager language (owner, 2026-10-05).
escrow.lockis new: Rust supplies every party's genesis and signing key, orders the terms canonically, and refuses a stranger or a repeated name.The frontend's decisions moved into Rust (owner, 2026-10-05; practice mode excluded):
session.unlockno longer opens the session for any caller; a locked wallet cannot change its lock; the preferences route refuses the lock's keys. The lock screen renders Rust's answers.sofi.create_vaulttakes the two tokens in either order.token.checkanswers whattoken.createwould refuse, field by field, and the wizard keeps no rules of its own. A ticker is counted in characters, as MR-SOFI-0303 states.Defects found and fixed on the way
token.burnnow parses the typed amount at the token's decimals (MR-SOFI-0349).createTokencould not tell Rust's refusal from a call that never came back, so the wizard's ask-again path for an ambiguous outcome never ran. The ingress boundary now throws aRustRefusalfor Rust's answers.How it is tested
ingress.rswrites them; each must equal the live answers). The escrow record comes from a funded device on the network's nodes. No test doubles.VERIFICATION_MATRIX.md.make lintgreen; the Android JNI changes pass an Android-target check. The full board is CI's.On the phones
Proven on a real Android device (Samsung A16, beta fleet, fresh wallet): the lock end to end (reads refused while locked, three misses then phrase only, a restart gives no try back, another wallet's phrase refused, this wallet's phrase opens it, cold start locked and the PIN opens it), and an escrow locked, decided and released on one phone (balance back to 100.00 ERA).
Between two phones that are each other's contacts: one locked 5 ERA on two outcomes decided by the other; the other found it by its verdict cell, decided "delivered" and released it, the locker's own release refused; 5 ERA moved. The vault card's buttons were relaid after the labels ran off them on the A16.
Recorded in
CONFORMANCE_GAPS.md§6.75 andVERIFICATION_MATRIX.md.🤖 Generated with Claude Code