Skip to content

security: beta pre-audit fixes (combined CORE, STORAGE, SOFI) - #1096

Merged
cryptskii merged 39 commits into
mainfrom
security/beta-pre-audit
Oct 2, 2026
Merged

cryptskii merged 39 commits into
mainfrom
security/beta-pre-audit

Conversation

@cryptskii

@cryptskii cryptskii commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

The owner's beta security pre-audit, combined into one PR across the CORE, STORAGE and SOFI sessions. Audited at main 076792eac. specs/requirements/CONFORMANCE_GAPS.md is the ledger: each finding is recorded with its exploit, its closure condition and its evidence, and is closed only with an adversarial test and a mutation control.

Landed so far

Item 2 (Critical): SoFi parent-root authentication (§6.62, SOFI)

The flaw.

  • A trader could name, as P's parent, a single-root claim it signed itself: a root it never held, with invented pre-balances under it.
  • Conformance verified the claim under its own key, and the facts mapped every single-root parent to compatible.
  • So the swap realized and walkers consumed the vault's key: a drain, and a mint on Close for a vault-owning attacker.

The fix.

  • A single-root parent counts only as what the trader's lineage holds at p: the claim final at K_root(p) and its root.
  • The verifier establishes that itself, from its own admitted position or the A8 walk (peer_root_at). It is the same check A8 and S15 already use; nothing new is added in economic/.
  • ParentPosition::SingleRoot { named, held, held_root }: the ladder computes compatibility from the evidence instead of asserting it.

Evidence.

  • Adversarial tests:

    • a claim the trader doesn't hold at p gives Invalid, and the key is Skipped;
    • the claim at another root gives Invalid, and the key is Skipped;
    • a parent not yet established is no fact.

    On the old code the first two resolved Ok(Realized).

  • Three mutations turn the tests red.

  • Lean: ParentState.singleNotHeld and theorem single_root_parent_not_held_is_invalid. Dropping the case from parentImpossible makes the theorem fail to check.

  • Still depends on item 1 (CORE): claims at K_root bound to the DevID key.

Item 6 (P0): a send names its recipient by device id (§6.63, SOFI)

The flaw. wallet.sendSmart resolved the recipient by alias. Aliases are not unique, so a send could pay whichever contact the database returned first under that alias.

The fix.

  • OnlineTransferSmartRequest reserves field 1 (recipient) and carries bytes recipient_device_id = 5, exactly 32 bytes.
  • The SDK takes those 32 bytes or refuses the request. get_contact_by_alias had no caller left and is removed.
  • The frontend sends the device id of the contact the user chose.

Evidence.

  • dsm_sdk::handlers::sender_admission_tests::a_send_pays_the_contact_chosen_not_the_first_of_its_alias: two contacts share an alias, and each is paid what was sent to it. On the old wire the chosen contact received 0.
  • Mutation: the handler resolves by alias again → red. Both payments go to one contact, and the second is refused.

Items 7, 8, 10 and 11: storage (§6.64, STORAGE)

Merged from fix/storage-dos-and-transport-hardening (5afda74d6):

  • Item 10. One concurrency limit for the node, request and header-read timeouts, a bounded pool, a bounded set-mate read, and concurrent immutable puts that no longer refuse each other.
  • Items 7 and 8. https:// only, in the env config and in every member client. Each member client trusts only the configured CA and accepts only a certificate naming the member id. The SDK harness serves its nodes over TLS.
  • Item 11. A spool position past what a spool can hold, and an envelope that never opens for this device, no longer stop the inbox read.

Every gate was mutation-tested. Open items for the owner are listed in §6.64.

Client workspace lock

dsm_client/deterministic_state_machine/Cargo.lock gains the three dependency edges the merge introduced: axum-server under dsm_sdk, hyper-util under dsm_storage_node, and tokio under tower-http. No versions changed. Without it, cargo test --locked in the client workspace refused to build.

Item 1 (P0): a position cell's occupant proves its own authority (§6.65, CORE)

DSM Amendment A10, SoFi Amendment S20, per the owner's ruling: "If an object can win/occupy a root position, the object itself proves authority for that position."

  • Root claims carry the device's AttA and are recognized only when their key derives the cell's DevID.
  • C_q occupies K_root(q) only when trader-signed (class 0x0062).
  • F carries AttA and binds to the device from its own bytes.
  • Four mutation controls.

Items 15 and 5: decoders bounded by their input; a nonce spent only in its relationship (§6.67, CORE)

Item 12 (P1): junk can no longer exhaust a SoFi walk or hold a key (§6.66, SOFI)

Closed (96b870fdd), each with an adversarial node or unit test red on the old code, and 7 mutation controls:

  • 12a. The walk's budget chunks walks and ends none, and conformance item 5 reads exactly K^(a-1). Junk at 17 keys used to freeze a vault.
  • 12b. An exercise's closure objects must be the objects its references name. A junk-closure copy first at the leader used to hold the key.
  • 12c. K_ful(q) is read only from the leader's log. One junk fulfillment at any member used to wedge every read of a position.
  • 12d. A conditional parent held by another claim selects no root, so the position is Invalid and its keys are skipped. It used to be "not established" forever.

In progress, 12j (critical, found by an adversarial pass). Which fulfillment holds K_ful(q) depended on when the precommit it names is published. A trader could make early verifiers see its trade realized and later ones see the position unresolvable, with the vault key frozen. A probe on the nodes confirmed it. The fix, agreed with CORE, decides K_ful(q) from F's bytes alone and pairs the cells by F's id. CORE is writing the S20 text; SOFI implements it with the probe as the test.

Open: 12e (the exercise carries the signed C_q, MR-SOFI-0362), 12f (a parent on a lineage known invalid), and 12g, 12h and 12i (owner rulings: a defeated route's other legs, withheld evidence, locators that can be flooded in advance).

Merged tree, at 2de9fa75b

Check Result
dsm lib (--locked --release) 1389 passed, 0 failed
SDK suites (node e2e, admission, recipient dispatch, token create, faucet, frontier verification, realized records, sofi flow, reads, advance and relay, route seats, storage node and io, b0x, core_sdk, client_db) 454 passed, 0 failed (one test failed at harness boot under load and passed alone)
make lint exit 0
Real-code guard clean
Conformance evidence passes; §7 regenerated

Known harness flake: under load, the two-device harness's faucet funding claim can find the previous reserve release not yet final, and the harness treats that as fatal. The device resumes it; the harness should too. The fix goes in test_support (SOFI).

Folding in next

  • SOFI: 12j, 12e and 12f, then items 9, 13, 14 and 16, plus the inbox case STORAGE handed back (what a limit-capped sync means).
  • CORE: item 3 (credit sources held to account through their own segment), plus the S20 extension for 12j.

Pins

INTENT_PINS will be repinned once, from this PR's Code map, after the folds.

Security pre-audit, item 10, from the storage side. Each bound has a test
that holds the resource the way a hostile client would, and a mutation
control that turns it red.

- One count of requests in flight for the whole node. A router applies a
  layer to each route on its own, so ConcurrencyLimitLayer gave every route
  its own limit; GlobalConcurrencyLimitLayer shares one.
  (one_count_of_requests_in_flight_holds_across_every_route; per-route
  limit → red)
- A request time, outside the limit, so waiting for a slot is bounded too.
  `[http] request_timeout_secs`, 60 by default; answered 408.
  (a_request_whose_body_never_arrives_is_answered_408; disabled → red)
- A header-read bound on every connection, with the timer the HTTP library
  needs in order to keep any bound. axum-server never gave it one.
  `[http] header_read_timeout_secs`, 10 by default.
  (a_connection_that_never_finishes_its_headers_is_closed. Red without the
  bound, and red without the timer: hyper then drops the connection at once,
  which the test refuses as too early.)
- The pool is bounded: 32 connections, a 10 s wait. Closers queue on the
  node's own lock before they take a connection, so queued closers hold
  none. (closers_waiting_their_turn_leave_the_pool_to_everyone_else; a lock
  taken and dropped → red)
- A set-mate's answer is read no further than the largest ByteCommit
  encoding (221 bytes, derived from the fields).
  (a_set_mate_whose_answer_never_ends_is_read_no_further_than_a_byte_commit;
  uncapped → red)
- The store-wide SMT builds for a close and for a proof run off the
  request threads (spawn_blocking).
- The mirror write is durable like every other write.
- Health answers "postgres unavailable" and logs the error; it no longer
  sends the driver's message to whoever asks. (health test; old text → red)
- Spool keys are kept under their one canonical Base32 Crockford spelling,
  so a writer's spelling cannot put a message where its recipient never
  reads. (a_message_sent_under_another_spelling_of_its_key_reaches_its_spool;
  raw key → red)
- The immutable put no longer logs the caller's header text.
- Deploy: SSH host keys are accept-new, not ignored. node.key is owned by
  the uid and gid the image runs as, at 600, not 644. Both services' logs
  rotate (5 × 50 MB).

The timeouts are Durations handed to the HTTP and pool libraries. No node
source reads a clock; no_clock_reads stays green.

Node suite (release, Postgres): lib 39, bin 4, nine integration suites
including resource_limits 4/0 and bytecommit_chain 9/0. fmt, node clippy,
the guard and the clock gate are clean.
insert_immutable_object_if_absent ran SERIALIZABLE (SELECT … FOR UPDATE,
then INSERT) with no retry. Concurrent puts of different objects failed
with serialization failures (40001), which the node answered as 500. SOFI
measured 6 of 16 on the rig node set.

The put is now read committed and idempotent:
- INSERT … ON CONFLICT (addr_b32) DO NOTHING;
- when nothing was inserted, the held tuple is read to tell an identical
  copy from a conflicting one.

Different objects never contend, and puts at one address wait for each
other at the insert. It runs through begin_durable_write, like every other
write.

Test: puts_made_at_once_are_each_taken. Three rounds of 64 distinct objects
put at once, each taken, and 16 identical puts at once, each acknowledged
under one address. Mutation control: the serializable put → red, 29 of 64
refused with 500 in round 0. The corruption test still reports a different
tuple at an address. immutable_store_round_trip: 8/0.
… K_root(p), never a claim P carries

Pre-audit item 2 (Critical), audited at main 076792e.

The flaw:
- A trader could name, as P's parent, a single-root claim it signed for
  itself, with a root it never held and invented pre-balances under it.
- Conformance verified that claim under the key it carries.
- The facts mapped every single-root parent to compatible.
- So the swap realized, and walkers took the vault's key as consumed: a
  drain, and a mint on Close for a vault-owning attacker.

The fix:
- A single-root parent now counts only as what the trader's lineage holds
  at p: the claim final at K_root(p) and its root. The verifier establishes
  this itself, from its own admitted position or the A8 walk
  (trader_root_at -> peer_root_at). No separate mechanism is added.
- ResolvedParent carries the claim that holds K_root(p).
- ParentPosition::SingleRoot { named, held, held_root }: the ladder
  computes compatibility (held exactly as named, at P's root) and
  impossibility (its negation), so the old SingleRoot => true/false arms
  are gone.
- The S15 resolver hands over the walk's own authenticated parent for both
  kinds of claim.

Tests:
- Adversarial: a claim the trader doesn't hold at p, and the claim at
  another root, each make the position Invalid and the key Skipped. These
  were Ok(Realized) before the fix.
- A parent that is not established is no fact.
- Three mutations turn the tests red.
- Lean: ParentState.singleNotHeld, with theorem
  single_root_parent_not_held_is_invalid. It fails to check if the case is
  dropped from parentImpossible.

Suites: dsm sofi 240/0, SDK node suites 80/0, lint 0, guard clean.
Still depends on pre-audit item 1 (CORE: claims at K_root bound to the DevID key).
…; junk cannot wedge an inbox

Security pre-audit, items 7, 8 and 11 from the storage side. Each gate has
a test and a mutation control that turns it red.

- https:// only, in the env config loader and in every member client
  (storage_node_sdk::require_https). Tests:
  network::tests::an_endpoint_that_is_not_https_is_refused and
  a_member_endpoint_that_is_not_https_gets_no_client. Mutation: http
  accepted → both red.
- A member client trusts only the CAs the env config names, not the system
  store. It accepts only a certificate naming the member id as a DNS
  subject alternative name, whatever address it dialled (NamesTheMember).
  MemberClient::new(member_id, endpoint) builds its own client, so a
  member's name and its connection's identity cannot disagree. The inbox
  client holds one such client per member endpoint. The five fleet certs
  were read in a handshake and each carries DNS:dsm-node-N for its member.
  Test: a_members_latest_bytecommit_is_its_own_or_there_is_none (a client
  for dsm-node-2 at dsm-node-1's node is refused on the certificate).
  Mutation: verify the dialled address → red.
- The SDK test harness serves its nodes over TLS from a per-set CA, each
  certificate naming its member and 127.0.0.1, as the fleet's do. There is
  no localhost exception. point_sdk_at names the CA as the bundled config
  does.
- Transport errors carry their causes, so a refused certificate says why.
- Inbox:
  - a node-supplied spool position is used only if a spool could hold it.
    u64::MAX + 1 panicked the poller, and a position past i64::MAX aborted
    the read (a_position_past_what_a_spool_can_hold_is_none; unchecked →
    red).
  - an envelope that will not open with this device's key is passed over,
    not stopped at for good
    (what_never_opens_for_this_device_holds_up_nothing_behind_it; no
    advance → red). The key is derived once per read.
  - the poller's running flag is cleared however its task ends
    (a_poller_that_panics_is_no_longer_running; never cleared → red).

CONFORMANCE §6.62 records items 7, 8, 10 and 11, the decisions left to the
owner (paging, write budgets, the mirror cap, committing the CA) and the
sync-limit question left to SOFI.

dsm_sdk whole suite (release, Postgres, TLS harness): 1093/0. Clippy for
dsm_sdk and dsm_storage_node --all-targets -D warnings is clean. fmt, the
guard and conformance_evidence are clean.
Pre-audit item 6 (P0), audited at main 076792e.

The flaw: the send screen chose a contact by its device id but sent the
contact's alias. wallet.sendSmart resolved that alias to the first matching
row, and aliases are not unique, so a send to the second "Bob" paid the
first.

The fix:
- OnlineTransferSmartRequest reserves field 1 (recipient) and carries
  bytes recipient_device_id = 5 [(dsm_fixed_len)=32].
- The SDK takes those 32 bytes or refuses the request, and resolves
  nothing by alias. get_contact_by_alias had no caller left and is removed.
- The frontend sends the chosen contact's device id, the key SendTab
  selects it by.
- Practice mode finds recipients by device id.

Test: a_send_pays_the_contact_chosen_not_the_first_of_its_alias. Two
contacts share an alias, and each is paid what was sent to it. It was red
on the old wire. Resolving by alias turns it red again.

Frontend: type-check clean, 7 suites, 51 tests. make lint exit 0.
…audit

STORAGE's pre-audit work, tip 5afda74: item 10 (node resource bounds),
the node's concurrent immutable-put abort (read committed, ON CONFLICT DO
NOTHING), items 7 and 8 (https only, members known by certificate) and
item 11 (inbox junk). Its CONFORMANCE section is renumbered §6.64.
…ependencies

STORAGE's branch added dsm_sdk -> axum-server (a dev-dependency) and
dsm_storage_node -> hyper-util, plus tower-http's timeout feature. These
were recorded in the root lock only. The client workspace builds
dsm_storage_node as a dev-dependency, so its lock carries the same three
edges. No version changed.
Owner ruling, 2026-10-01, on the audit finding that anyone could occupy
any identity's next root cell: "If an object can win/occupy a root
position, the object itself proves authority for that position."

Every object recognized at K_root(q) is now signed by a key that, with the
AttA the object carries, derives the device it names. Nothing is fetched.

- **Root claim.** `EconomicRootClaimBody` moves to schema 2 and carries
  `claimant_att_a`. `decode_and_verify_economic_root_claim` refuses a claim
  whose key and AttA do not derive `trader_devid`
  (`KeyIsNotTheNamedDevices`).
- **C_q.** It occupies K_root(q) only as the trader-signed
  `SignedSofiResolutionClaim` (class 0x0062, signed under
  `DSM/sofi/resolution-claim-sign/v1`). A bare C_q is refused by name.
  `sofi::signature::verify_resolution_claim` checks the signature and the
  binding, and `sign_resolution_claim` refuses a key that does not derive
  the named device.
- **Identity at a root cell.** A conditional claim is identified by its
  derived body, so readers keep comparing `derive::resolution_claim(P, F)`.
- **Producers.** The admission flow signs its root claim with this device's
  AttA. `sofi_register` signs the trader's C_q. The relay carries the
  trader's signed C_q from the cell, because it cannot author one.
- **Tests.** The SoFi fixture device and the peer device have ids derived
  from their keys. Adversarial tests:
  - a root claim and a C_q signed by another device (with its own AttA or
    the victim's) name no cell;
  - an unsigned C_q names no cell;
  - a squatter's claims written first at every seat leave the cell open
    until the trader's own claim holds it.
…parent_named

Item 2 (§6.62) replaced resolve.rs's parent_named with held_at, which counts
a walked root only at the position it reached, and moved the comparison of
the claim P names with the claim held into the facts (establish_ground).
The manifest row still named parent_named, so the requirement map refused
the root query and #1096's Code map failed before producing a map.

The row now names held_at, evidenced by
what_a_walk_holds_counts_only_at_the_position_it_reached. Mutation control
(2026-10-01, restored): held_at taking a root of another position turns that
test red. The matrix and the §8 row say what parent_named's test covered
that no test covers now, a conditional parent held by another fulfillment,
which pre-audit item 12 restores.
The owner's item-1 rule reaches the fulfillment cell: an F at K_ful(q)
can win the position, so it proves its own authority for it.

- `TraderFulfillmentBody` carries `claimant_att_a`.
- `registration::fulfillment_proves_the_device` checks that F's key with
  that AttA derives the cell's device, from F's bytes alone.
  `names_fulfillment_key` applies it before it looks up P, so a
  foreign-key F neither holds the cell nor makes a reader fetch a P it
  names.
- Its P key equality then binds P's key too.
- The SDK's `build_fulfillment` takes this device's AttA and refuses a P
  whose key does not derive the device it names.
- Test: a squatter's own P and F naming the victim's device, verifying
  under the squatter's key, name no cell with either AttA, even with that
  P in hand. The trader's own F names it.
- The independent encoder and every F fixture carry the AttA.
- **DSM Amendment A10.** It follows the explainer's root-claim paragraph.
  "Only Alice can sign one" is enforced from the bytes in hand: every
  occupant of a device's position cell carries its key and AttA, and is
  recognized only when its signature verifies and `derive_devid(key, AttA)`
  is the cell's DevID.
- **SoFi Amendment S20.** It sits at §17.4.
  - K_root(q) holds the trader-signed C_q (class 0x0062).
  - F carries AttA, and is bound before P is fetched.
  - The exercise carries the trader's signed C_q.
  - A relayer completes the pair with the trader's C_q and never authors
    one.
  - It names every section it amends.
- **MASTER.** §1 re-pinned. MR-DSM-0277 and MR-SOFI-0360 to MR-SOFI-0362
  added from the amendments. §7.1 entry added.
- **CONFORMANCE §6.65 (item 1).** MR-DSM-0277, MR-SOFI-0360 and
  MR-SOFI-0361 are Met with mutation controls. MR-SOFI-0362 is Missing: the
  exercise's C_q lands on #1096. §7 totals regenerated.
- **VERIFICATION_MATRIX.** Rows for the three gates and their mutations.
- **Code.**
  - `RootClaimSettlement::Lost` boxes its holder: the claim grew by its
    AttA.
  - The node test's re-aimed F carries the honest F's AttA.

Board: dsm lib 1381/0 (four runs), dsm integration all pass, SDK
integration all pass, make lint 0. SDK lib: 1045/4 under load; the four
(funding writes Preserved, not final, while the shared Postgres was busy)
pass 4/0 when rerun alone.
The negative-test column of MR-SOFI-0361 put P and F in backticks inside
its prose, which the evidence check read as test names. Plain words now;
conformance evidence passes.
…ion for good

Pre-audit item 12. Four paths by which cheap junk froze a vault or a
trader's position, each with a node or unit test that went red on the
old code:

- The walk's budget ended walks. Conformance item 5 read K^(0) to
  K^(min(a,16)-1), though it needs K^(a-1) alone, so a fulfillment at
  attempt 17 never had the key it names. Extending a chain and a leg's
  liveness walk both stopped at Continue. Junk exercises at the first 17
  keys of a root therefore froze the vault there. Item 5 now reads exactly
  K^(a-1), and every caller resumes at the cursor (SoFi §23.6).
- Recognition counted an exercise's closure objects and checked nothing
  else, so a copy of an honest exercise with other closure bytes, written
  first at a leg's leader, held the key and could never be classified. Each
  closure object must now be the object its reference names, by
  conformance item 8's rule (§17.5).
- A registration read fetched the precommit of every fulfillment any seat
  held at K_ful(q), and failed when one could not be read, so a value at
  any member, written at any time, stopped every read of that position.
  Only the leader's log decides the cell: precommits are fetched only for
  its copies, in arrival order, until one names the key.
  route_chain::leader_copies is the one rule the leader link and this read
  share.
- A conditional parent another claim holds at p was "not established"
  forever, so no key its exercise held was ever skipped. It selects no root,
  ever: ConditionalNoRoot, impossible, skipped without evidence (§23.3).

Tests: junk_at_more_attempt_keys_than_one_walk_examines_is_walked_past,
an_exercise_carrying_other_bytes_than_its_closure_holds_no_key,
nothing_but_the_leaders_first_fulfillment_decides_k_ful (node);
a_conditional_parent_another_claim_holds_selects_no_root (facts). Seven
mutation controls, one per gate, each red and restored.
…onship

Security pre-audit items 15 and 5.

- **Operation decoder.** `dec_vec_bytes` no longer preallocates from a
  sender-written count. A recovery operation claiming u32::MAX authority
  signatures asked for ~103 GB before decoding any: a panic on 32-bit,
  and an abort under a refusing allocator. The vector now grows only as
  elements decode.
- **Proof decoders.** `DevTreeProof` and `SmtInclusionProof` compute their
  length checks in u64. On 32-bit, `count * 32` wrapped for count 2^27
  and the copies indexed past the input.
- **Nonce scope.** A transfer nonce is spent under
  `relationship_nonce_hash(rel_key, nonce)` (`DSM/relationship-nonce/v1`)
  in the canonical apply's identity, in `spent_nonces` and in staging.
  - Before, a nonce was spent device-wide. Nonces derive from public
    inputs, so one contact could spend another contact's predicted nonce
    and have that contact's real transfer dropped as decided.
  - Replay is still refused: the same transfer carries the same nonce in
    the same relationship, and the (relationship, parent) record holds.
  - The unused device-wide `is_nonce_spent` and `mark_nonce_spent` are
    deleted.
- **Tests:** the three hostile decoder inputs, and a nonce spent in one
  relationship staying unspent in another.
…t items 15, 5)

- Item 15. Each fix and its test are recorded. So is what this 64-bit
  macOS host cannot show: the old failures were 32-bit or
  allocator-specific, so no mutation turns these tests red here. Out of
  this round's scope: recovery capsule counts. The phone's response sizes
  are STORAGE's.
- Item 5. The cross-sender nonce finding, its relationship-scoped fix,
  and its mutation control (the scope removed: the cross-relationship
  test red).
…nt (A8)

Security pre-audit item 3. Owner ruling 2026-10-01, "Segment now,
proofs later".

- **The gap.** A credit's source step was validated, but the source's own
  history was authenticated by root claims alone. "Admitted" means only
  that the source signed its root, so two wallets of one owner could
  create value one hop back: C registers an invented root, pays its own A
  out of it, and A pays honest B.
- **The fix.** `source_step` validates the source's segment from this
  receiver's frontier for it with `chain_through`, exactly as a payer's
  segment is validated. That segment's own credits' sources are held to
  account the same way, back to frontiers this verifier already
  validated. The source step itself is a debit that names no source.
- **Fail closed.** When the ancestry exceeds the shared walk budget before
  reaching a frontier, the result is Incomplete, never weaker evidence.
- **Renames.** The provenance types now say what they do: `SourceAncestry`,
  `DebitHasNoSources`, `StepRole::Source`, `source_step`.
- **Tests.**
  - The source's step at 1 is validated from its witness and successor
    evidence. This was the old "one hop and no further" test, now
    inverted.
  - A source's segment stops at the frontier the receiver holds: nothing
    of C at or behind it is read.
…hing

Owner ruling 2026-10-01, pre-audit item 3, test 4: a source's ancestry
that exceeds the walk budget before a frontier this verifier holds is
Incomplete, never accepted on weaker evidence.
…ecords one

A frontier becomes trusted only once the whole segment to it has passed
(A8, owner ruling 2026-10-01, pre-audit item 3 test 5).
…old a key (§6.66)

§6.66 records item 12's findings apart from their fixes:
- 12a–12d closed by `96b870fdd`, each with its adversarial test, red on
  the old code, and its mutation controls.
- 12j: a fulfillment's registration at K_ful(q) depended on when the
  precommit it names is published. A trader can make early verifiers
  see its trade realized and later ones see the position unresolvable,
  with the vault key frozen. Confirmed by a probe on the nodes. The
  closure is agreed with CORE: decide K_ful(q) from F's bytes alone,
  per the item-1 ruling. In progress.
- Open: 12e (the exercise carries the signed C_q, S20), 12f (a parent
  on a lineage known invalid), 12g and 12h (owner rulings), and 12i
  (locators computable in advance).

§8 corrections:
- MR-SOFI-0244 was Met on the pure walk while three of its callers
  stopped at the budget.
- MR-SOFI-0213, MR-SOFI-0170, MR-STOR-0132 and MR-SOFI-0241 name the
  new gates and tests.
- MR-SOFI-0234 is Partial on 12f. §7 is regenerated.

Matrix rows for items 2, 6 and 12, the first two of which had none.
…y/beta-pre-audit

CORE's item 1 (f5cd5d9): a position cell's occupant proves its own
authority (DSM Amendment A10, SoFi Amendment S20).

Resolutions:
- CONFORMANCE §6: §6.65 (item 1) sits between §6.64 and §6.66.
- §7 is regenerated.
- The item-12 test helper `signed_by_b` passes the AttA that F now
  carries.
…ems 15, 5)

CORE's items 15 and 5: core decoders are bounded by their input, and a
transfer nonce is spent only in its own relationship.

Resolutions:
- CONFORMANCE §6: §6.67 follows §6.66.
- §7 is regenerated.
- The domain tag count is 354. Both branches raised it from 352 to 353,
  which git merged as one change. The tags are the signed resolution
  claim's digest (S20) and the relationship nonce (item 5);
  `the_declared_domain_tag_count_is_the_expected_one` counts 354.
…ce (A8)

Each credit source's segment that passes whole leaves a frontier at the
source step. The walk reuses it for later sources of the same identity, and
the accept transaction records it beside the sender's, so the next credit
from that source validates only the suffix (owner ruling 2026-10-01:
"Cache/update validated frontiers so subsequent interactions validate only
the new suffix"). validate_peer_lineage returns ValidatedPeerLineage.
…ment; §6.68

Owner ruling 2026-10-01 (pre-audit item 3, "Segment now, proofs later"),
quoted in A8. The One hop bullet becomes Credit sources; S15's SetupValid
wording follows. MR-DSM-0273-0275 and MR-SOFI-0347 rewritten; §1 re-pinned.
Tests 1 and 2 of the ruling recorded as owed (hostile-device harness).
…tes (12j)

A reader passed over an F whose P it did not hold yet, so an F naming an
unpublished P, written ahead of the F a trader traded with, flipped the
position between verifiers reading before and after the P appeared, and
froze the vault key the trade's exercise held (SOFI's node probe).
Applying the item-1 ruling ("the object itself proves authority for that
position"): K_ful(q) is decided from F's bytes alone; the pair is matched
by FulfillmentId(F); an F whose K_ful(q) another F holds in any state is
lost; the C_q body is compared with derive(P, F) where P is in hand, a
difference losing F and making the position Invalid. MR-SOFI-0168, 0343,
0360, 0361 rewritten and Partial until SOFI's change lands on #1096.
…em 3)

CORE's item 3: a credit's source is held to account through its own
segment back to a frontier the receiver holds; validated source frontiers
are recorded with the acceptance (§6.68).

Resolutions:
- MASTER §1 pins are recomputed from the merged specs (explainer
  871fb1a…/4323, SoFi 49cd9eb…/2689). The pin note names both
  amendments.
- The verification matrix keeps both sides' rows and drops the
  one-hop-source row that item 3 replaced.
- peer_lineage's new source-step test uses `peer_d()`, the derived
  device item 1 put in place of the `PEER_D` constant.
…extended for 12j)

CORE's S20 extension: K_ful(q) is decided from F's bytes alone. The pair
is matched by FulfillmentId(F). A position is lost when another F holds
the K_ful(q) leader link in any state. The C_q body is checked against
derive(P, F) where P is in hand. MR-SOFI-0168, 0343, 0360 and 0361 are
Partial until SOFI's registration change lands.

Resolutions:
- MASTER §1's SoFi pin is recomputed from the merged spec
  (8360842…/2693), and the pin note names the extension.
- §7 is regenerated.
…enever it is read (12j)

Pre-audit item 12, 12j (SoFi Amendment S20, extended). Recognizing a
fulfillment at K_ful(q) used to need the precommit it names, and a precommit
not published yet (Resolved::None) made the reader pass over that
fulfillment. A trader could write F_a, naming an unpublished P_a, ahead of
the F_b it traded with:
- verifiers reading before P_a appeared saw F_b registered and the trade
  realized;
- verifiers reading after saw F_a holding the cell and F_b neither
  registered nor lost, with the vault key frozen.

Now:
- names_fulfillment_key decides from F's bytes alone: it verifies under its
  own key, names q, and its key and AttA derive the cell's DevID. No
  precommit is read, so read_registration fetches none, and
  NAMED_PRECOMMIT_BUDGET and PrecommitLookup go.
- The pair is matched by FulfillmentId(F). Registration::Held names the
  fulfillment that holds K_ful(q)'s leader link before the pair is final,
  and RegistrationRead carries the id of the claim at K_root(q).
- RegistrationRead::standing_of (P in hand) loses F to any other
  fulfillment holding K_ful(q)'s leader link, in any state (§23.1), and to
  any claim at K_root(q) other than derive(P, F). That covers rule 4:
  a claim naming F with another body. The facts read the pair through it.
- The S15 resolver compares the held claim with derive(P, F) once P is in
  hand: Invalid when they differ.
- route_chain's public leader_copies wrapper had no caller left and goes;
  leader_link keeps its iterator.

Tests:
- a_position_reads_the_same_before_and_after_a_precommit_is_published
  (node): red on the old code by the probe, where the read flipped and the
  key froze.
- a_fulfillment_is_lost_to_any_other_leader_and_to_any_claim_not_its_own
  (unit).
- Mutations: another fulfillment's leader link losing nothing turns both
  red; skipping the body check of a registered pair turns the unit test red.
…d pair can be registered from it (12e)

SoFi Amendment S20 makes C_q trader-signed, so no relayer can author it. A
trader that wrote its exercise at a vault's key and withheld its position
pair held that key for good: its F could never register, rung 0 waited, and
every later trade at the vault waited behind it.

- SofiExercise carries the trader's signed C_q (class 0x0062) after F.
- recognize_exercise requires F to prove P's device from its own bytes, and
  the carried claim to be recognized exactly as K_root(q) recognizes it
  (root_claim_naming) with the body derive(P, F). An exercise failing either
  is nothing at the key.
- The install returns the signed C_q it wrote, and the exercise carries those
  bytes.
- sofi_relay::relay_exercise registers the fulfillment whose exercise holds a
  vault key from the exercise's bytes alone, and carries the exercise.
  relay_fulfillment carries the pair with the claim K_root(q) holds, or the
  one its exercise carries. relay_position_pair had no caller and is gone.
- The cell value cap rises from 256 KiB to 320 KiB (route_chain::MAX_VALUE_LEN,
  the proto bounds, MAX_EXERCISE_BYTES): a two-leg exercise measured 218,735
  bytes before C_q, about 269 KB with it. STORAGE agreed; the node's cap
  follows MAX_ENTRY_BYTES.

Tests: node test
an_exercise_whose_trader_withholds_its_pair_is_registered_from_its_own_bytes;
unit tests an_exercise_carries_the_traders_signed_claim_of_its_own_p_and_f and
an_exercise_whose_fulfillment_does_not_prove_the_traders_device_is_nothing.
Mutations, each red: the relay ignoring the exercise's claim; the body
comparison dropped; the claim's verification dropped; the device check
dropped.
Conflicts resolved as an integration, not by picking a side:
- tour/practiceMode.ts: main's ERA-at-two-decimals work (PRACTICE_ERA_HELD,
  the faucet amount as people count ERA, ready() before every read, debits in
  turn, the refusal shape) with #1096's item 6 (the online send names its
  recipient by device id; a simulated send finds its contact by device id
  only). The stale wholeAmount line from before #1095 is gone.
- practiceMode.test.ts: main's new tests send to the practice contact's
  device id.
- CONFORMANCE_GAPS.md: main's §6.60 and §6.61 (the perf PR's reserved
  numbers) ahead of §6.62 to §6.68; §7 regenerated.

Also: ci/sofi_relay_is_party_neutral.sh checks relay_exercise's entry
(committed set and vault key) where it checked relay_position_pair, which
12e removed; MR-DSM-0213 cites relay_exercise.

Main's frontend fails the real-code guard on its own: 21 lines from #1095
await the owner's baseline. This merge carries them unchanged.
…over TLS

#1094's a_fetch_passes_over_a_member_that_answers_first_with_other_bytes
stood up member 0 as a plain-HTTP responder at the scheme of the member's
endpoint. Under https-only member clients that scheme is https, so the
device's TLS ClientHello reached a responder that read it as an HTTP head
("the request ended before its head did"). The responder never answered,
the relays never opened, and the fetch returned None. The test was red at
#1096's head.

- The node set keeps its CA, and NodeSet::tls_for (with Fleet::tls_for)
  issues the certificate a member serves, naming it, for a test that
  stands a member up at another address.
- The substitute serves that certificate (tokio-rustls, already an SDK
  dependency), so the device reaches it as it reaches the member, and the
  test again exercises fetch_verified.
- Mutation control: fetch_verified keeping the first answer whatever it
  hashes to → red ("other bytes" kept). The test is load-bearing again.

storage_node_sdk tests: 8/0 (release, Postgres, TLS harness). fmt and
dsm_sdk clippy --all-targets are clean.
…om its exercise before passing the key (12e)

Owner ruling, 2026-10-01: "If a vault key is held by an exercise whose pair
is not yet registered, the relayer MUST use the signed C_q carried by that
exercise to register the trader's missing pair before advancing past that
held key. ... After successful pair registration, the relayer retries
progression."

- The walk a trade, route or close makes for its live attempt
  (walk_for_attempt) checks each held key it would pass: when the exercise's
  pair stands Pending (neither registered nor lost), it registers the pair
  with relay_exercise, carrying the trader's bytes exactly, and walks again.
  A registered or lost pair, or one the reads do not decide, is not written.
- A trade and a close walk each vault's chain past withheld pairs before
  drafting (chain_past_withheld_pairs), so they draft at the head those
  registrations leave.

Tests:
- an_exercise_whose_trader_withholds_its_pair_is_registered_from_its_own_bytes
  now has the vault's owner close the vault. A claim A signs for B's pair
  occupies nothing; A's close registers B's pair from B's exercise, closes at
  the head B's trade left, and B's position realizes.
- a_held_key_whose_pair_is_registered_is_passed_without_writing_the_pair:
  every member holds the same values at B's two cells before and after A's
  close.

Mutations, each red: the walk never relays (A's close is RetriesExhausted
behind the key); a registered pair counts as withheld (A's close is refused
after relaying it round after round).
… security/beta-pre-audit

# Conflicts:
#	dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/node_e2e_tests.rs
…a withheld pair

- §6.66 12e: the record (exploit, closure, the owner's 2026-10-01 ruling
  "Choose \"Next trader.\" ... MUST ... before advancing past that held key",
  the cell value cap raised to 320 KiB and why), and six mutation controls.
- §8 MR-SOFI-0362 Missing -> Met. MR-DSM-0213 cites relay_exercise.
- §6.64: the new per-entry maximum beside the cell GET paging item (STORAGE).
- §6.65: its open note points at the closure.
- VERIFICATION_MATRIX: the 12e row.
- §7 regenerated.
@cryptskii
cryptskii marked this pull request as ready for review October 2, 2026 02:55
@cryptskii
cryptskii merged commit e736dbc into main Oct 2, 2026
20 of 24 checks passed
@cryptskii
cryptskii deleted the security/beta-pre-audit branch October 2, 2026 02:56
cryptskii added a commit that referenced this pull request Oct 2, 2026
…r, and names §17.5 and §33

Owner ruling, 2026-10-01 (pre-audit 12e), quoted verbatim in S20 as the
producer's MUST: the next operation at a vault registers a withheld pair
from the signed C_q its exercise carries before advancing past the key, and
retries. S20's amends list now names §17.5 (the exercise's new field) and
§33. Written on #1096 at CORE's request.

MASTER: §1 re-pinned (SoFi 7792794…, 2694 lines); MR-SOFI-0362 carries the
MUST.
cryptskii added a commit that referenced this pull request Oct 2, 2026
…ce id

After #1096 an online send names its recipient by device id, never by
alias, and practice mode finds the contact the same way. The sandbox test's
practice send now goes to PRACTICE_CONTACT_DEVICE_ID.
cryptskii added a commit that referenced this pull request Oct 2, 2026
#1096 carried the owner's correction to DSM Amendment A8 for sources: a
credit's source is validated through its own segment, not one hop back and
no further. It renamed the frontier test to match:
a_credits_source_is_validated_one_hop_back_and_no_further →
a_credits_source_is_validated_through_its_own_segment.

The two manifest rows citing it still named the old test, so their evidence
named no test and neither row could be repinned. MASTER's text for both rows
already states the corrected rule. The pins are taken in the repin commit
that follows, with the evidence change accepted.
cryptskii added a commit that referenced this pull request Oct 2, 2026
…med unpinned

#1096 merged before its repin, so main's Code map read 622 PIN_STALE, 1
UNPINNED and 1 ORPHAN_PIN. Taken from this PR's own Code map (run
36963250731, at 79048b7) and an evidence log of the same commit: 326
tests, 326 passed. Each key goes through intent_pins' own rules (prepare,
judge_repin, refuse_evidence, seal_new), with the map loaded once:
- 622 repinned. All code-class except four:
  - MR-DSM-0273 and MR-DSM-0275: their evidence names the test #1096
    renamed (accepted: evidence).
  - MR-SOFI-0234, two keys: its §8 status moved Met → Partial in #1096,
    recorded at §6.66 (12d) (accepted: status).
- MR-SOFI-0356 held_at pinned; the parent_named orphan unpinned.

make requirement-map-intent MAP=… INTENT_BUILT=android,node: 0 failing
rows, 0 failing pins, 635 PINNED.
cryptskii pushed a commit that referenced this pull request Oct 2, 2026
The Code map read 2 sentinels lost, so the real map was not clean before
planting and four mutation cases failed with it. Both subjects changed on
purpose in #1096, so the rows change here, as the sentinel file says:

- OneHop became SourceAncestry (a credit's source is validated through its
  own segment, the corrected DSM A8). It is built in the same function, the
  same way, and dispatched through the same &dyn ProvenanceResolver, so its
  anchored_policy_bytes reads as OneHop's did: REACHED_VIA_DISPATCH.
- PrecommitLookup and its BTreeMap impl are gone (12j: registration reads
  no P at all). The row is removed. The map still reads 14 other
  IND_AMBIGUOUS_SELF_TYPE definitions on android, and the rule keeps its
  fixture case (one-twin-is-renamed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
cryptskii pushed a commit that referenced this pull request Oct 2, 2026
rules::every_rule_has_a_row_and_every_citation_exists failed on the
previous commit: two rule rows cited the sentinels it changed.

- trait-dispatch cites SourceAncestry's anchored_policy_bytes, OneHop's
  successor.
- IND_AMBIGUOUS_SELF_TYPE cites none, with the gap saying why: its one real
  sentinel, PrecommitLookup's impl on BTreeMap, went with #1096 (12j). The
  map still reads this code on android; a sentinel is taken from a real
  reading of it at the next map. Its unit tests, fixture readings and
  mutation case stand.

requirement_map's unit tests: 99 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
cryptskii added a commit that referenced this pull request Oct 2, 2026
…ade real, repin after #1096 (#1098)

- Android instrumented tests: the instrumented config names https://127.0.0.1:8080; the SDK's loader refuses any endpoint that is not https (#1096).
- Real-code guard: #1095's 21 flagged frontend lines made real (fixture hunks reversed to the baseline's lines; practice refusals and protocolDefined computed from Rust's answers).
- Repin after #1096: 622 code moves, held_at pinned, the parent_named orphan unpinned; MR-DSM-0273 and MR-DSM-0275 cite the test #1096 renamed.
- Code map sentinels: OneHop's row follows it to SourceAncestry; PrecommitLookup's row is removed with #1096 (12j), and rules.tsv records the gap for IND_AMBIGUOUS_SELF_TYPE.
cryptskii added a commit that referenced this pull request Oct 2, 2026
… beta (#1097)

- Practice mode is contained at the bridge: while it runs, only reads (READ_ROUTES, read bridge methods, display preferences) reach native code; writes are answered by practice or refused.
- The guided tour covers the online beta; its practice contact is a code Rust reads (identity_routes test pins the frontend's copy).
- A practice send names its recipient by device id, as #1096 requires.
- Practice addContact answers accepted from the contact it holds (real-code guard).
- Merged with #1098: practice ERA's protocolDefined is Rust's balance.list row, so a practice send reads balance.list as well as wallet.amount.
cryptskii added a commit that referenced this pull request Oct 10, 2026
…r, and names §17.5 and §33

Owner ruling, 2026-10-01 (pre-audit 12e), quoted verbatim in S20 as the
producer's MUST: the next operation at a vault registers a withheld pair
from the signed C_q its exercise carries before advancing past the key, and
retries. S20's amends list now names §17.5 (the exercise's new field) and
§33. Written on #1096 at CORE's request.

MASTER: §1 re-pinned (SoFi 7792794…, 2694 lines); MR-SOFI-0362 carries the
MUST.
cryptskii added a commit that referenced this pull request Oct 10, 2026
…ard lines are real

Main at 9ba1684 was red on four jobs. This commit fixes three; the fourth
(Code map) is the repin that follows.

- Android instrumented tests. androidTest/assets/dsm_env_config.instrumented.toml
  named http://127.0.0.1:8080. The SDK's config loader now refuses any
  endpoint that is not https:// (#1096), so initSdk never installed the app
  router and every instrumented test failed. The suite never reaches a node,
  so the endpoint only has to pass the loader: https://127.0.0.1:8080.
- Real-code guard and frontend type check (the type check runs the guard
  first; tsc itself was clean). #1095 left 21 flagged lines:
  - In four frontend tests (EnhancedWalletScreen.events, offlineCash,
    faucetClaim, wallet), #1095 edited grandfathered fixture lines to write
    ERA at two decimals. These tests check that the frontend passes through
    what Rust renders, and the frontend does no decimal math, so #1095's
    hunks in those files are reversed. Their lines are again the ones the
    baseline holds, and the tests check the same mapping. #1096's change to
    the wallet screen test is kept.
  - practiceMode's two refusal answers state success/accepted as whether the
    debit went through: !('refused' in result).
  - Practice ERA's protocolDefined is Rust's: ERA's row in the router's
    balance.list, which lists ERA at any balance. The frontend never decides
    it from a ticker. The ingress test that writes the practice wallet's
    record of Rust's answers now records balance.list too, and asserts Rust
    lists ERA as protocol-defined; the record is rewritten from the live
    ingress. practiceMode.test checks practice ERA against Rust's row.
    Mutation: practice ERA not taking Rust's value → red.

Full-tree guard: exit 0. tsc: 0 errors. jest over the tour, wallet screen,
offline cash, faucet and wallet tests: 75 passed plus the new practice
test. The dsm_sdk ingress test passes against the rewritten record. fmt is
clean.
cryptskii added a commit that referenced this pull request Oct 10, 2026
#1096 carried the owner's correction to DSM Amendment A8 for sources: a
credit's source is validated through its own segment, not one hop back and
no further. It renamed the frontier test to match:
a_credits_source_is_validated_one_hop_back_and_no_further →
a_credits_source_is_validated_through_its_own_segment.

The two manifest rows citing it still named the old test, so their evidence
named no test and neither row could be repinned. MASTER's text for both rows
already states the corrected rule. The pins are taken in the repin commit
that follows, with the evidence change accepted.
cryptskii added a commit that referenced this pull request Oct 10, 2026
…med unpinned

#1096 merged before its repin, so main's Code map read 622 PIN_STALE, 1
UNPINNED and 1 ORPHAN_PIN. Taken from this PR's own Code map (run
36963250731, at f0c1af8) and an evidence log of the same commit: 326
tests, 326 passed. Each key goes through intent_pins' own rules (prepare,
judge_repin, refuse_evidence, seal_new), with the map loaded once:
- 622 repinned. All code-class except four:
  - MR-DSM-0273 and MR-DSM-0275: their evidence names the test #1096
    renamed (accepted: evidence).
  - MR-SOFI-0234, two keys: its §8 status moved Met → Partial in #1096,
    recorded at §6.66 (12d) (accepted: status).
- MR-SOFI-0356 held_at pinned; the parent_named orphan unpinned.

make requirement-map-intent MAP=… INTENT_BUILT=android,node: 0 failing
rows, 0 failing pins, 635 PINNED.
cryptskii pushed a commit that referenced this pull request Oct 10, 2026
The Code map read 2 sentinels lost, so the real map was not clean before
planting and four mutation cases failed with it. Both subjects changed on
purpose in #1096, so the rows change here, as the sentinel file says:

- OneHop became SourceAncestry (a credit's source is validated through its
  own segment, the corrected DSM A8). It is built in the same function, the
  same way, and dispatched through the same &dyn ProvenanceResolver, so its
  anchored_policy_bytes reads as OneHop's did: REACHED_VIA_DISPATCH.
- PrecommitLookup and its BTreeMap impl are gone (12j: registration reads
  no P at all). The row is removed. The map still reads 14 other
  IND_AMBIGUOUS_SELF_TYPE definitions on android, and the rule keeps its
  fixture case (one-twin-is-renamed).
cryptskii pushed a commit that referenced this pull request Oct 10, 2026
rules::every_rule_has_a_row_and_every_citation_exists failed on the
previous commit: two rule rows cited the sentinels it changed.

- trait-dispatch cites SourceAncestry's anchored_policy_bytes, OneHop's
  successor.
- IND_AMBIGUOUS_SELF_TYPE cites none, with the gap saying why: its one real
  sentinel, PrecommitLookup's impl on BTreeMap, went with #1096 (12j). The
  map still reads this code on android; a sentinel is taken from a real
  reading of it at the next map. Its unit tests, fixture readings and
  mutation case stand.

requirement_map's unit tests: 99 passed.
cryptskii added a commit that referenced this pull request Oct 10, 2026
…ce id

After #1096 an online send names its recipient by device id, never by
alias, and practice mode finds the contact the same way. The sandbox test's
practice send now goes to PRACTICE_CONTACT_DEVICE_ID.
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