Repository navigation
feat(sofi): port the hardware-proven trader swap + owner reconcile fixes to main - #673
Merged
cryptskii merged 6 commits intoAug 19, 2026
Merged
Conversation
On a handset the SoFi hub's SWAP tile did nothing. The click fired, the handler ran, no error appeared in the console or logcat, and the screen did not change. LIQUIDITY and MAIL from the same hub worked. `'swap'` was missing from `VALID_NAV_TARGETS`. `navigate` drops an unknown target with a bare `return`, so the request vanished silently. Everything else about the path was correct and had been all along: `'swap'` is a member of the `ScreenType` union, `AppScreenRouter` has handled `case 'swap'` for as long as the hub has existed, and `SofiHubScreen` declares it as a brick target. The compiler saw a legal target; the runtime Set refused it; nothing reported the disagreement. This was the trader's only entrance to the production swap surface, which is why it went unnoticed - every swap exercised so far went through the dev screens or direct route calls. The test walks the hub's three brick targets and requires each to actually land. A type union and a runtime allowlist that are not derived from one another will drift again; this pins the three that a user can press. Verified non-vacuous: removing `'swap'` fails it.
`SwapTab` sent `new TextEncoder().encode(inputToken.trim())` as the pair identity. A vault's pair is two 32-byte CPTA policy commits, so the trader was asking for a market named by the UTF-8 bytes of whatever was typed. Those two can never be equal. `syncVaultsForPair` and `listAdvertisementsForPair` matched nothing, every quote ended in "No liquidity advertised", and no advertised vault was reachable from the production swap surface at all. This is the same defect that was fixed on the owner's side — LiquidityScreen stopped naming pairs by label when the picker moved to policy commits — and the trader's half of that pair was left behind. Discovering it needed both halves running against one real vault: a funded vault on one handset and a trader on another, which is the first time the two sides were exercised together. Both fields now decode Base32 Crockford to exactly 32 bytes and refuse anything else, naming the field that was wrong. There is no ticker path and no fallback: a label can name more than one token — two distinct tokens have shared the ticker RIGB in this repo — so resolving one here could name a different asset than the trader meant while every signature downstream still verified. The datalist now suggests the ANCHORS of held tokens, labelled with their ticker, because the anchor is what the field means. Suggesting tickers invited precisely the input the field cannot accept. An anchor for a token you do not hold is still accepted by hand — you do not hold what you are buying. The tests typed 'DEMO_AAA' and 'ERA' into those fields, so they encoded the broken contract and passed under it. They now use real 52-character anchors.
The trader anchored a commitment, the route reported success, and settlement then failed with "settlement slot not held: this trader's pending pointer is not visible in the slot". Storage said why: `sofi/vault-pending/` was EMPTY at every sequence, while the vault's advertisement sat next to it under `sofi/vault/`. No pointer had ever been written, for any vault. `route.publishExternalCommitment` accepted two shapes: a bare `ExternalCommitmentV1`, or a wrapper carrying the signed RouteCommit so it could derive the per-hop pending pointers. With no RouteCommit it published the X anchor ALONE and returned success. The frontend sent the bare shape, so that branch was the live path on every trade. Two things were wrong and only one of them was the missing pointer. The signed RouteCommit was expected to travel out to the caller and back. It is protocol state - hops, vault ids, sequences, the bound amounts - and it has no business in the render layer. `route.signRouteCommit` now retains it against its own X, and `route.publishExternalCommitment` resolves it there. The caller sends an anchor and nothing else; the shape it cannot get wrong is the one it no longer supplies. `PublishExternalCommitmentRequest` is deleted, not deprecated. Pointer publication is mandatory. An X with no retained route is refused, a route with no hops is refused, and a run where any hop fails to publish returns an error naming how many of how many failed. It used to log a warning at `log::warn!` and return OK, so the only way to discover a missing pointer was to reach the settlement gate and be turned away - which is precisely what happened on hardware, one screen and several minutes after the moment that could still have been retried. The producer and the consumer are now tested against one route and one slot: sign through the router, publish, read the pointer back out of storage, assert the key names that vault, that sequence and that trade, then drive the real `claim_settlement_slot` and require it to see exactly that pointer. Nothing covered both halves before, which is how they were free to disagree. Verified non-vacuous: suppressing the pointer write fails it.
…o main) Port of a0485443 from chore/bench-inspectable-release-build (hardware-proven 2026-08-01, never merged). `dlv.listOwnedAmmVaults` now reports the REAL number of settlements this owner has not folded (`pending_unapplied`, was hardcoded 0 under a comment saying reconciliation was not wired) plus each one's external commitment (`pending_x`), read from storage against this head's own reserve leaves; the LiquidityScreen shows the count and a Reconcile action that folds them in order and stops at the first failure. Port notes vs the original: - `AmmVaultSummaryV1.pending_x` is field 19, not 17 — main assigned 17/18 to the ticker labels after Aug 1. TS proto regenerated to match. - The pending list is now EXPLICITLY ordered by each receipt's `new_sequence` before it is returned. Since #670 a fold consumes exactly the current parent (the reserve-leaf `parent_sequence` claim), so generation N must fold before N+1; the storage key layout happens to sort that way, but fold order is a protocol requirement, not a property of a path string. - Placed in main's reshaped summary loop (ticker resolution + policy-digest republish fields), computed after the vault lock is dropped.
…(ported to main) Port of fc7659e5 from chore/bench-inspectable-release-build (hardware-proven 2026-08-01, never merged). `dlv.listOwnedAmmVaults` enumerated the in-memory DLVManager, which holds only what the current process created — so a wallet that had merely been restarted showed the owner "My vaults (0)" over a funded, published vault. `rehydrate_all_amm_vaults` was correct, tested, and had zero production callers. The route now reads the authoritative pair: the `amm_vault_records` row for identity and policy, the head's encumbered reserve leaves for reserves and sequence, joined through `rehydrate_amm_vault`. A record that is non-canonical, names another owner, carries an unknown enforcement mode, or whose legs are absent or disagree makes that vault UNAVAILABLE — absence is never rendered as zero. Repopulating the manager is rejected deliberately (the record does not carry parameters_hash / creator_signature / encrypted_content; synthesising them would place a complete-looking object nobody computed in front of every consumer). Port notes: main's post-Aug-1 additions are carried into the persisted-state loop — ticker display labels (resolved from the commit; identity is never the label) and the ordered `pending_x` / real `pending_unapplied` from the previous commit. Prerequisite for the LP-offline hardware proof: an LP that force-stops and relaunches must still see its vault to fold the market's settlements.
This was referenced Aug 18, 2026
cryptskii
deleted the
feat/sofi-port-trader-swap-and-owner-reconcile-to-main
branch
August 19, 2026 04:09
cryptskii
added a commit
to cryptskii/dsm
that referenced
this pull request
Sep 10, 2026
… proof Hardware-proved 2026-08-18 on the 3-phone rig against the live Alibaba fleet (schema v4, APK carrying deterministicstatemachine#670 + deterministicstatemachine#672 + the deterministicstatemachine#673 SoFi UI ports): LP (8XK) created SOFI (CPTA), funded a Required SOFI/ERA vault (25,000,000 / 100, 30 bps) — head: SOFI 100,000,000 -> 75,000,000, ERA 290 -> 190, exactly the two reserve leaves at gen 0 — then was force-stopped. With the LP process dead, T1 (9FF) and T2 (5GN) settled four generations through the production SwapTab (0->1 T1 20 ERA -> 4,156,244; 1->2 T2 15 ERA -> 2,309,794; 2->3 T1 10 -> 1,274,633; 3->4 T1 8 -> 899,882), every quote exact against the COMPOSED reserves the trader derived from the prior receipts alone. T2 then confirmed a quote bound to gen 3 after T1 had consumed it: refused by the delegation guard ("is at generation 4 but the route binds generation 3 — that parent is already consumed"), T2's root byte-identical, one unreceipted pointer left on the fleet and nothing else. The LP relaunched, saw its vault (persisted-state listing) with "4 settled trades to reconcile", tapped Reconcile: leaves at gen 4, reserves 16,359,447 / 153 == funding + Σ inputs − Σ outputs, consumption rows 0..3 by four distinct receipts, LP spendable untouched (no second debit). scripts: - rig_dlv_market.py — step-wise CDP driver over the PRODUCTION UI (create-token, anchors, create-vault, vaults, swap [quote|execute|full], reconcile, balances, offline/online). CDP substitutes for finger taps only; every protocol step is the real route. - dsm_head_decode.py — host-side DeviceState v0x06 decoder for a pulled dsm_client.db: balances + reserve leaves, legs named via amm_vault_records + BLAKE3 leaf keys (b3sum). No hex. - rig_settle_foreground.py — clears Android permission dialogs + the lock prompt so the WebView owns the foreground (fresh-onboarding trap). - dlv_market_rig_proof.sh — read-only proof over the three pulled DBs + the fleet's public object listings; 17 assertions, all PASS.
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.
Why
The SoFi trader path on
mainhas been broken since 2026-08-01: the four fixes that made funded creation → trader settlement work on silicon (TRACE-2026-08-01-062/063) were committed tochore/bench-inspectable-release-buildand never merged. Onmaintoday: the SWAP brick is dead-routed,SwapTabnames the pair by label bytes (no liquidity discoverable),route_commit.tspublishes X without the pending pointer (settle refused "slot not held"), and nothing callsdlv.reconcile— the owner cannot fold settled trades. Surfaced while staging the multi-generation LP-offline hardware proof for #670/#672.What lands (original commits, original authorship)
0bf28f3efix(sofi): the SWAP brick could never open —'swap'added toVALID_NAV_TARGETS.6979b35cfix(sofi): the trader named the pair with the bytes of a label — SwapTab sends 32-byte CPTA anchors.d0b707d1fix(sofi): a settlement pointer is required, and Rust holds the route — Rust retains the signed RouteCommit keyed by its own X;route.publishExternalCommitmenttakes an anchor only and publishes X + RC + the mandatorysofi/vault-pending/{vault}/{seq}/{X}pointer, refusing if any hop is unpublished.PublishExternalCommitmentRequestdeleted.fc7659e5fix(sofi): the owner's vaults come from persisted state, not a cache — ported by hand (conflicted):dlv.listOwnedAmmVaultsreadsamm_vault_records+ the head's reserve leaves viarehydrate_amm_vault, never the in-memory DLVManager, so an LP that force-stops and relaunches still sees its vault (the restart-persistence bug from finding_rehydration_never_called_restart_probe). Absence is never rendered as zero.a0485443feat(sofi): the owner can see settled trades, and fold them — ported by hand (conflicted with feat(dlv): enforce consume-once per vault generation + prove reserve authority #670/feat(dlv): settle against the composed vault state — delegated liquidity across generations #672's reshapeddlv_routes.rs):dlv.listOwnedAmmVaultsreports the realpending_unapplied+pending_x(was hardcoded 0); LiquidityScreen shows the count and a Reconcile action that folds in order and stops at the first failure.Port deltas vs the originals:
AmmVaultSummaryV1.pending_xis field 19 (main assigned 17/18 to the ticker labels after Aug 1); TS proto regenerated.new_sequence. Since feat(dlv): enforce consume-once per vault generation + prove reserve authority #670 a fold consumes exactly the current parent, so generation N must fold before N+1; the storage key layout happens to sort that way, but fold order is a protocol requirement, not a property of a path string.Gates
route_routes7/7,vault_rehydration11/11,dlv_routes15/15 (serial); frontend 82/82 on the touched suites;make lintclean (fmt,clippy --all-targets -D warnings, npm lint — the one npm warning is pre-existing on main inRecoveryPipelineScreen.tsx); proto guards + codegen enforce pass;no_clock_and_no_json+production_safety_checks(prod clippy + TLA+) pass.Prerequisite for the multi-generation LP-offline hardware proof (owner directive 2026-08-18).