Skip to content

SoFi rig fixes (S16, S19, wallet records), ERA two decimals (S18), storage liveness and no clock - #1089

Merged
cryptskii merged 15 commits into
mainfrom
feat/sofi-vault-discovery-auto-setup-and-realize-records
Oct 1, 2026
Merged

cryptskii merged 15 commits into
mainfrom
feat/sofi-vault-discovery-auto-setup-and-realize-records

Conversation

@cryptskii

@cryptskii cryptskii commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

The 2026-10-01 phone-rig fixes from all three sessions, in one PR at the owner's direction. It replaces CORE's #1087 and STORAGE's #1088, both merged into this branch:

1. SoFi Amendment S16: a vault is found by its tokens, and its setup is the first step of the first trade through it

Rig finding. sofi.findRoute searched only the vaults the trader had already set up with, so the rig's first trade (9FF through 8XK's vault) went through only because 9FF set up by hand.

The token index.

  • Creating a vault now indexes its genesis preimage under vault_token_locator(t) for each of its two tokens.
  • TAG_DSM_SOFI_VAULT_TOKEN_LOCATOR is new; the tag count tripwire goes from 351 to 352.
  • Verifier::vaults_of_token keeps a candidate only when vault_genesis accepts it and the accepted market pairs the token. Junk bytes, a forged genesis, and a real vault of another pair are passed over.
  • A candidate the reads cannot decide makes the discovery partial, never Invalid.

Path search.

  • findRoute reads the input and output tokens' indexes, not the trader's relationships, and needs no setup.
  • SofiFindRouteResponse.search says whether the search saw every vault. A partial search with no route is an error in the app, never "no route".
  • This replaces §6.31's behaviour, where an unestablished vault made the whole search an error.

Setup.

  • set_up_with admits the setup transaction for any vault on the route that the trader has no setup with: one per vault, in hop order.
  • It runs only after the route is priced at the walked heads, so a route that cannot be built admits nothing.
  • The owner's first Close does the same. Later trades reuse the setup.
  • sofi.setup is no longer a route (envelope field 121 reserved).

sofi.vaults (new route, envelope field 126). Lists the vaults the device created, from the VaultCreation leaves of its validated root, each walked to its head: reserves, fee, generation and status. Storage → DLVs shows them above the dBTC vaults.

2. Beta blocker: another trader's conditional parent stalled every walk

The bug.

  • A vault walk that met another trader's exercise whose P names its trader's earlier SoFi position as parent answered ParentUnresolved and stopped.
  • So after any trader traded twice through a vault, every other device's walk of that vault stalled. The owner saw stale reserves, and the next trader's attempt went stale.
  • The rig didn't hit it because 9FF traded once.

The fix.

  • The walk now resolves that parent through peer_root_at. It's a new entry point in economic/peer_lineage.rs, written to CORE's spec and reviewed by CORE.
  • It walks the trader's lineage from the walker's frontier, with the position itself resolved by S15's PeerPositionResolver, as an interior conditional position.
  • The parent counts only as the position and fulfillment P names (parent_named).

Two related changes.

  • Resolving a position bounds each vault's chain at the parent its leg names (chain_until). Unbounded, it walked into the very exercise whose parent it was resolving, and the stack overflowed.
  • Verified roots are kept per reads context, so k exercises by one trader don't each re-walk its lineage.

Known cost, recorded in CONFORMANCE §6.53. A walker without a frontier for a trader walks that trader's lineage from activation, with a 512-step budget. A trader with more than about 500 positions is Incomplete for such walkers. Whether a walk should record frontiers is an owner question.

3. SoFi Amendment S19: a route chains or splits

Owner ruling (2026-10-01): "It should be no different. It just happens to be the same token," and "It should work either way."

Core rule.

  • A Swap route's hops either chain, or all trade the intent's one pair through distinct vaults, with inputs and outputs summing to the intent in checked arithmetic.
  • validate_swap decides the shape against the intent; a split that doesn't sum is Invalid::SplitDoesNotSum.
  • route_endpoints is the one rule the producer states the intent by.

Producer.

  • The search weighs each pair of direct vaults as a split, beside single hops and chains, and proposes the route that gives the most.
  • The split share is searched over the inputs both legs price.
  • The trade plans the same split again at the heads it walks.

4. The wallet's records of SoFi (rig rulings; display caches, not requirements)

  • History. Token creation, vault creation, setup, and a realized trade, route or close each write a history row naming every token moved with its signed amount (TransactionInfo.moves, types 7–11). The wallet renders each line.
  • Balances. On every such event the projection of each moved token is rebuilt from the head, keeping any lock. A realized trade shows in balance.list at once, not after a restart.
  • Amounts. SoFi request amounts are text in token units (*_entered), parsed by parse_display_amount_to_base_units against each token's committed decimals. Reported amounts carry *_display. sofi.trade and sofi.route name the output token, and the route must give it.

5. ERA has two decimals: SoFi Amendment S18 (CORE, was #1087)

Owner ruling (2026-10-01): ERA is divisible to hundredths. The fee model is unchanged: the fee comes out of the input and the trader pays it.

  • Policy. Decimals 2. The supply is 8,000,000,000,000 base units (80,000,000,000.00 ERA).
  • Commitment. New commitment NNG176RZ6ACTWCDPRNYHXZK2DCZ72SPA9Q6XWGRGQ9JGKZYTESG0, a clean cut. The ERA reserve id hashes the commitment, so the new ERA has its own reserve chain and the nodes need no wipe.
  • Unchanged in whole ERA. The faucet pays 10,000 base units (100.00 ERA) and the creation fee is 1,000 (10.00 ERA).
  • One conversion. The SDK's parse_display_amount_to_base_units and format_base_units_for_display convert amounts. The SoFi routes use them too (§4), so MR-SOFI-0349 is Met here.
  • Storage-member HTTP client (SDK). A member has 10 s to accept a connection and 30 s to answer a request. On the rig, a position write sat with no member answering and no error. A member that never answers is now a member that did not answer, which every flow already handles. It is a transport bound: no validity or ordering decision reads it.
  • Tests. Funding is stated through economic_fixtures::whole_era(n). The vertical_validation trace token_manager_balance_replay (feat(core): ERA has two decimals (SoFi Amendment S18); storage requests time out #1087's red CI) is fixed.

6. Storage: a silent set-mate cannot hold the mirror sync; no node source reads a clock (STORAGE, was #1088)

The liveness bug (CONFORMANCE §6.54).

  • The node's set client had no connect or request bound, and mirror_sync holds a node-wide one-at-a-time lock. A set-mate that accepted a connection and never answered held that sync, and every later one, open indefinitely.
  • The fix:
    • set_client gets a 5 s connect bound and a 10 s request bound. A fetch that times out fails like an unreachable set-mate (502, lock released) and stores and orders nothing.
    • MAX_SYNC_CYCLES drops from 1024 to 128, so one sync stays inside the SDK's 30 s bound.
    • route_seats::read_cell asks every seat at once.
  • Test: a_set_mate_that_never_answers_fails_the_sync_and_frees_it. With the timeouts removed it goes red.

MR-STOR-0008, Partial → Met.

  • dsm_storage_node::no_clock_reads parses every node source with syn and refuses Instant, SystemTime, UNIX_EPOCH, chrono and tokio::time.
  • Mutation controls, each red: a grouped-import tokio::time sleep in the spool, and Instant::now() in db/pg.rs.
  • ci/no_clock_and_no_json.sh loses two node exemptions.

Records only. MR-STOR-0131, 0133, 0138 and 0094 re-verified Met; 0134 stays Partial.

Tests (release, on the shipped Postgres node backend)

At the combined head 83b6f61 (all three branches merged):

  • Node suites. dsm_sdk node_e2e, sender_admission, realized_records and sofi_flow: 35 passed, 0 failed.
  • Core. dsm lib, the sofi::, economic::, core::token, core::state_machine and common::domain_tags tests: 335 passed, 0 failed.
  • Validation traces. dsm_vertical_validation: 28 passed, 0 failed (token_manager_balance_replay fixed; feat(core): ERA has two decimals (SoFi Amendment S18); storage requests time out #1087 was 27/1).
  • Storage. dsm_storage_node no_clock_reads: 2 passed, 0 failed.
  • Frontend. jest 52 suites, 303 tests, all passing; tsc clean.
  • Static checks. make lint exit 0. real_code_guard clean. ci/conformance_evidence.py: 805 rows, 0 failures.

Before the merge, on the SoFi branch alone: the whole dsm crate, lib and integration, 1,616 passed and 0 failed. CORE's and STORAGE's boards are in their sections above.

New SoFi tests.

  • Node: discovery_passes_over_what_is_not_a_vault_of_the_token; a_route_search_that_cannot_see_its_vaults_says_so (renamed); every_sofi_route_reaches_its_producer (no manual setups, sofi.vaults, the owner's walk past B's second trade); a_realized_trade_shows_in_balances_and_history_at_once; one_order_fills_through_two_vaults_of_the_same_pair.
  • Core: a_route_chains_or_splits_and_its_endpoints_follow; a_split_whose_legs_sum_to_the_intent_is_not_refuted_by_its_shape; a_split_that_does_not_sum_to_the_intent_is_invalid; only_the_position_and_fulfillment_p_names_resolve_its_parent; a_frontier_recorded_at_the_position_is_its_root_and_nothing_is_read; the_activation_root_names_no_parent.

Mutation controls (each restored byte for byte, each with a named test red):

  • discovery without the market check;
  • trade without set_up_with;
  • no projection rebuilt on realize;
  • peer resolution with an unbounded chain (stack overflow);
  • no split candidate;
  • a partial discovery reported complete;
  • parent_named accepting any fulfillment, or skipping the position;
  • peer_root_at standing below the position;
  • no route read as a split;
  • unchecked split sums.

Records

  • MASTER. MR-SOFI-0350–0359 added beside CORE's MR-SOFI-0349; MR-SOFI-0255 updated (the routes); §1 re-pinned to the merged SoFi specification; 928 canonical requirements.
  • CONFORMANCE. §6.53 (SoFi) and §6.54 (STORAGE's, renumbered), plus rows for each new requirement and updates to 0255, 0256 and MR-DSM-0219. MR-SOFI-0349 is Met. Totals regenerated.
  • Other files. VERIFICATION_MATRIX rows, INTENT_MANIFEST rows. INTENT_PINS will be repinned from this PR's Code map.

After merge

  • Rig phones need a wipe and re-provision. ERA's identity changed, so nothing under the old commitment carries over.
  • Then the three-phone run:
    • Phone A creates token RIGT with a deep ERA/RIGT vault and sends RIGT to phone B.
    • B opens a second ERA/RIGT vault.
    • Phone C, set up with nothing, swaps an amount that fills best split across both.
  • Frontend fixtures that still describe ERA as decimals: 0 (practice mode, some jest mocks) are queued for the frontend sweep.

Coordination

… base units

Owner ruling, 2026-10-01: an amount is entered and shown in whole tokens
with the token's decimals after a point, e.g. `10000.00` for a
two-decimal token, never as base units with the zeros typed by hand.

- `parse_token_units(text, decimals) -> u128`:
  - whole tokens, plus up to `decimals` fraction digits;
  - a bare integer is whole tokens;
  - refused: empty input, any character that is not a digit or the
    point, a second point, more fraction digits than the token has, and
    overflow.
- `format_token_units(base, decimals)` always shows every decimal place,
  and round-trips through the parser.

Every route and screen that takes or shows an amount goes through these,
with the decimals from the token's committed policy.
dsm_sdk's wallet_routes already owns the conversion between typed
amounts and base units: parse_display_amount_to_base_units and
format_base_units_for_display. It does exact string arithmetic against
the token's decimals, and it already serves sends, balances, history and
offline cash. 2d529fe added a second copy in Core, and two owners of
one rule is what that module's comment forbids. The SoFi routes' amount
fields go through the SDK's functions instead.
…ts time out

Owner ruling, 2026-10-01, from the phone-rig run: ERA is divisible to
hundredths.

- **Spec.** SoFi Amendment S18; MR-SOFI-0335 rewritten; MR-SOFI-0349
  added (Partial until the SoFi routes take amounts in token units).
  MASTER §1 re-pinned.
- **ERA's policy.** It carries decimals 2, and its supply in base units
  (8,000,000,000,000, which is 80,000,000,000.00 ERA). Its commitment is
  NNG176RZ6ACTWCDPRNYHXZK2DCZ72SPA9Q6XWGRGQ9JGKZYTESG0, a clean cut from
  JXPMPGJH…. Nothing under the old commitment carries over.
- **The faucet** pays 10,000 base units (100.00 ERA), and the token
  creation fee is 1,000 (10.00 ERA). The amounts people know are
  unchanged.
- **Test fixtures.**
  - The two-device fixture funds in whole ERA, one faucet claim per
    payout.
  - The reserve tests and the current-state test use the payout
    constant instead of a literal.
- **The storage-member HTTP client** connects within 10 s and finishes a
  request within 30 s. On the rig, a position write sat with no request
  answered and no error. A member that never answers is now a member
  that did not answer.
The dependent sweep for SoFi Amendment S18. Funding is in whole ERA
through `economic_fixtures::whole_era`, which scales by ERA's committed
decimals. A test that speaks in ERA therefore keeps its meaning. Sends,
burns, trades and vault reserves stay in base units, as written. Comments
that called a base-unit amount "10 ERA" now say so.

- Balances that come from the faucet or the creation fee are stated as
  `whole_era(n) ± base units`. Covers the faucet, sender admission,
  bilateral finality, offline step, node e2e and frontier verification
  tests, plus the restart and projection-repair tests.
- Display expectations are at two decimals: history "1.00" and "100.00";
  the inbox preview "0.10 ERA"; the pending step "0.03"; the transaction
  render "-1.00"; the balance row and the frontend ERA fixture "264.00" /
  "80000000000.00".
- ERA's reported facts: decimals 2, and the anchor is the new commitment
  NNG176RZ6ACTWCDPRNYHXZK2DCZ72SPA9Q6XWGRGQ9JGKZYTESG0.
- The offline load is entered as "20.00", which is 2,000 base units.

The 41 SDK tests the ERA change turned red now pass:
126 passed / 0 failed across their modules, release.
…ead asks every seat at once

Node side:
- The set client (set_client::pinned_set_client) had no connect or
  request timeout, and mirror_sync holds a node-wide one-at-a-time lock. A
  set-mate that accepted the connection and never answered held the sync
  and the lock open indefinitely, stalling every later mirror sync on the
  node.
- It now connects within SET_MATE_CONNECT_TIMEOUT (5 s) and answers within
  SET_MATE_REQUEST_TIMEOUT (10 s), or the fetch fails, as an unreachable
  set-mate does: BAD_GATEWAY, with the lock released. These are transport
  liveness bounds; a fetch that times out stores nothing and orders
  nothing.
- MAX_SYNC_CYCLES drops from 1024 to 128, so one sync of a member far
  behind stays well inside the SDK's 30 s per request. Every cycle is kept
  as it is fetched, so later syncs continue.

SDK side: route_seats::read_cell asked the five seats one after another, to
read values, to close cycles and to sync mirrors. A seat that does not answer
cost one client timeout per seat in each loop. The three fan-outs now run at
once (join_all), with the answers kept in route order, so the evidence Core
evaluates is unchanged.

Test: bytecommit_chain::a_set_mate_that_never_answers_fails_the_sync_and_frees_it
(a set-mate that accepts and never answers; two syncs each answer
BAD_GATEWAY within 3× the request bound). Mutation control: the set-client
timeouts removed → red ("sync 0 hung on a set-mate that never answers"),
restored.

Release, on Postgres: bytecommit_chain 8/0. route_seats, faucet flows and the
SoFi trade and pay node e2e tests 21/0. fmt, clippy -D warnings (dsm_sdk,
dsm_storage_node), the real-code guard and the clockless gate are clean.
…ws re-verified

MR-STOR-0131, 0133, 0138 and 0094 move Missing → Met and 0134 Missing →
Partial: each Missing note described a codebase before route chains. The
cited tests pass at this tree. 0134 stays Partial because the writer never
records a taken empty.

MR-STOR-0140 cites the silent set-mate test, and the matrix records its
mutation control. MR-STOR-0008 notes the set client's transport bounds.
Every node source is parsed with syn, reading paths, `use` trees and macro
bodies. Any clock or timer is refused: Instant, SystemTime, UNIX_EPOCH,
chrono, tokio::time. A comment or string never counts. The self-test covers
grouped, renamed and glob imports and a macro body; a first token-window
draft missed the grouped import.

Mutation controls, red and then restored:
- a grouped-import tokio::time sleep in the spool;
- Instant::now() in db/pg.rs.

ci/no_clock_and_no_json.sh loses two node exemptions: the spool, which
reads no clock, and a rate limiter that no longer exists. A clock read in
the spool now fails it too.

MR-STOR-0008 Partial → Met.
…(S16), routes that split (S19), the walk past another trader's SoFi parent, and the wallet's records of SoFi

Phone-rig rulings, 2026-10-01.

SoFi Amendment S16. A vault's genesis is indexed under each of its tokens;
Verifier::vaults_of_token keeps a candidate only when vault_genesis accepts
it and its market pairs the token, and an undecided candidate makes the
discovery partial. sofi.findRoute reads the two tokens' indexes, needs no
setup, and says when its search was partial. set_up_with admits the setup
transaction ahead of the first trade, route or close through a vault, after
the route is priced. sofi.setup is no longer a route; sofi.vaults lists the
device's vaults at their heads, shown under Storage -> DLVs.

Beta blocker: a walk that met another trader's exercise parented on that
trader's SoFi position stopped (ParentUnresolved), so after any trader traded
twice every other device's walk of the vault stalled. The walk now resolves
that parent through peer_root_at (economic/peer_lineage.rs, reviewed by
CORE), counted only as the position and fulfillment P names; resolving a
position bounds each vault's chain at the parent its leg names (chain_until),
and verified roots are kept per reads context.

SoFi Amendment S19 (owner: "It should be no different. It just happens to be
the same token"). A Swap route's hops chain or split across vaults of one
pair, summing to the intent; the search proposes the split when it gives
the most, and the trade plans it again at the heads it walks.

The wallet: token creation, vault creation, setup, trade and close write a
history row naming every token moved (TransactionInfo.moves) and rebuild the
moved tokens' projections from the head, so a realized trade shows at once.
SoFi request amounts are entered in token units and parsed against each
token's decimals; reported amounts carry their display form.

Records: MR-SOFI-0350-0359, CONFORMANCE §6.53, VERIFICATION_MATRIX,
INTENT_MANIFEST; SoFi spec re-pinned.
The token_manager_balance_replay trace asserted 61 and 39, which are the
faucet payout of 100 less the 39 it moves. ERA's payout is now 10,000
base units (SoFi Amendment S18), so the trace failed in CI
(workspace-rest).

The final balances are now the payout less the transfers' sum, and the
transfers' sum, the same rule the per-step conservation check already
states.

dsm_vertical_validation: 28 passed / 0 failed, release.
…route-chain-records' into feat/sofi-vault-discovery-auto-setup-and-realize-records

# Conflicts:
#	specs/requirements/CONFORMANCE_GAPS.md
…e SoFi PR

One PR for the three branches: the SoFi rig fixes, STORAGE's mirror
liveness and route-chain records (#1088), and CORE's ERA with two decimals
(#1087).

- MASTER: CORE's MR-SOFI-0349 beside MR-SOFI-0350–0359; §1 re-pinned to the
  merged SoFi specification (S16, S19 and S18); 928 canonical requirements.
- CONFORMANCE: MR-SOFI-0349 is Met. The SoFi routes take amounts as text in
  token units and report their display form, which was the part S18 left
  Partial. Totals regenerated.
- node tests: CORE's whole_era arithmetic, and the new SoFi tests' ERA
  balances moved to whole_era.
@cryptskii cryptskii changed the title feat(sofi): vaults found by their tokens with setup inside the trade (S16), routes that split (S19), the walk past another trader's SoFi parent, and the wallet's records of SoFi SoFi rig fixes (S16, S19, wallet records), ERA two decimals (S18), storage liveness and no clock Oct 1, 2026
One repin for the SoFi, CORE and STORAGE changes together:
- 25 new rows pinned;
- 591 stale rows repinned (582 code-class, 9 with their evidence changed:
  the SoFi route rows whose cited tests moved);
- the orphan pin of the removed `sofi_flow::setup` unpinned.

Evidence, run at 83b6f61: the 15 tests of the new rows; 241 tests for the
425 stale keys from the SoFi and CORE changes; STORAGE's 105 tests (11 runs)
for the 167 keys from its files. All passed.
`make requirement-map-intent` on CI's code map: 0 failing rows, 633 pinned,
0 failing pins.
@cryptskii
cryptskii marked this pull request as ready for review October 1, 2026 07:27
@cryptskii
cryptskii merged commit 6fe113d into main Oct 1, 2026
27 checks passed
@cryptskii
cryptskii deleted the feat/sofi-vault-discovery-auto-setup-and-realize-records branch October 1, 2026 08:13
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