Skip to content

fix: the placeholder sweep, the SDK's success-for-nothing rows - #1039

Merged
cryptskii merged 23 commits into
mainfrom
fix/placeholder-sweep-sdk-success-rows
Sep 27, 2026
Merged

cryptskii merged 23 commits into
mainfrom
fix/placeholder-sweep-sdk-success-rows

Conversation

@cryptskii

@cryptskii cryptskii commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

What

The sixth chunk of the placeholder sweep (#1037 write-up, #1038 first five chunks): the SDK's success-for-nothing rows B-5 to B-9 of the ledger's §6.36. One verified fix per commit:

  • b0x: the online send's evidence carries exactly the signing key the route resolved from the signing authority (fail-closed, 64 bytes by the params check) or no evidence at all; the builder's re-derivation under the state lock, the caller-supplied substitute and the persisted app-state key with its empty default are gone (B-6).
  • Finality barrier: a frozen inbound half the store cannot decode is an error the barrier reports, not "nothing in flight" that a send could cross (B-7; the ledger row now names the real site, recipient_staging::counterparty_has_unconverged_inbound).
  • Reply window: a promoted reply row without its post-admission release is a logged local defect left unmarked; nothing goes out with an empty release (B-9).
  • Offline send: wallet.sendOffline answers BilateralTransferResponse (its own message: the prepare went out under this commitment) instead of the peer's BilateralPrepareResponse with every field but the commitment defaulted; the frontend reads that answer, treats any other payload as unexpected, and no longer decodes a reject the route never produced (B-8).
  • B-5 (execute_simplified_bilateral_transfer) is corrected in the ledger: no such function exists on main.

Seventh chunk (gates that gate nothing):

  • Android: the WebView's CORS proxy, its host allowlist and the test that locked it are deleted; nothing in the page fetches an external host (C-K4). With them go the CSP's tile, maplibre, dsm-wallet.io and localhost entries and the service worker's tile cache.
  • Frontend: getHeaders reads the bridge on every call; the window cache that never invalidated, and the tests' resets of it, are gone (C-F10).
  • SDK: the bootstrap-report op (BootstrapMeasurementReport envelopes and the finalize path gated on a caller-supplied trust level "from the C-DBRW entropy health test") had no sender; deleted with its helpers, Genesis v2 being the one bootstrap path (S-INGRESS).
  • Core: pbi.rs says what it does — the identity pair as two 32-byte arrays, checked for length and nothing else; the unused domain tag and the claims of hashing and sole trust are gone (S-PBI).

Eighth chunk (the frontend's fakes):

  • The accept path emits wallet.bilateralAccepted, naming the transfer, instead of wallet.bilateralCommitted with committed: true before the peer's confirm (B-F4).
  • A bridge error shows its own message with its code; the causes invented for codes 460, 404 and 408, which nothing emits, are gone (B-F7).
  • A session snapshot missing its lock or hardware status is refused as malformed instead of filled with false; the store keeps the snapshot as decoded (B-F9).
  • The page takes the bridge port once, from the empty-string handshake Kotlin posts with it; a later or handshake-less port delivery is ignored and logged (S-F14). In the port handler the response id is declared outside the try, so the catch's cleanup releases the pending entry it used to miss (the page half of D-CODEC).

Ninth chunk:

  • Kotlin bridge codec: BridgeEnvelopeCodec decodes and encodes through the generated protobuf classes of dsm_app.proto (request, response, error, app-router, preference and bilateral payloads, the envelope's error) instead of its own varint parser with hard-coded field numbers; Kotlin holds no wire decoder. A request without a method is refused. Tests that asserted the hand-rolled parser's private rules now assert protobuf's semantics (the Kotlin half of D-CODEC).

Tenth chunk:

  • Safety classification: the page classifies a deterministic-safety refusal by Rust's Error.source_tag (11) and reads the class from the error's context, at the two places every transport error surfaces (decodeFramedEnvelopeV3, the ingress unwrapper); the regex over message text and Kotlin's scan of the answer bytes (which never parsed a framed answer) are gone (D-SAFETY).
  • Core features: bluetooth, storage, threadsafe, jni, web-stack and formal gated only a name; deleted with the optional dependencies they carried (jni, tokio-stream, rocksdb, axum, tower), the build-info function that only listed them and the build script's stamps it alone read (C-C19). The lockfile loses rocksdb and its native build chain and nothing else.
  • Core dead helpers: deterministic_id.rs (hex ids), calculate_next_entropy and hash_blake3 (each reached only by its own test), init_crypto and the init_sphincs self-test it alone called are deleted (D-C15, E-C15–18 for the named items).

Verification

  • dsm_sdk lib storage::client_db::recipient_staging: 11 passed, the two new tests among them; mutation: the skip restored → a_staged_half_that_does_not_decode_is_an_error_not_absence red, restored.
  • dsm_sdk lib sdk::b0x_sdk::tests on the Postgres test nodes, single-threaded: 33 passed.
  • cargo ndk -t arm64-v8a check -p dsm_sdk --features jni,bluetooth: clean.
  • Frontend related jest (51 suites, 313 tests), type-check, lint: pass.
  • ci/conformance_evidence.py: pass. make lint: passed. Device: the APK with this chunk is installed on the four phones; on the relaunched 5GN, DSM launches without a crash and the wallet's SEND tab renders. The rig's wallets currently hold no contacts and no ERA, so an offline send could not be driven; the answer decode is covered by the frontend tests against the exact message the route now produces.
  • Seventh chunk: dsm lib pbi (11 passed); dsm_sdk lib ingress on Postgres, single-threaded (18 passed); cargo ndk … check: clean; ./gradlew compileDebugKotlin compileDebugUnitTestKotlin: clean; frontend related jest (51 suites, 326 tests; the identity suite 14 passed after the cache test became the no-cache test), type-check, lint: pass; the five repository gates and ci/conformance_evidence.py: pass; make lint: passed.
  • Eighth chunk: frontend related jest (83 suites, 511 tests), type-check, lint: pass; the five repository gates and ci/conformance_evidence.py: pass (no Rust or Kotlin change in this chunk).
  • Eighth chunk, device: the APK installed on the four phones; on the relaunched 5GN, DSM launches with no crash and the wallet screen renders its balances and its identity, both fetched over the bridge port taken with the new handshake.
  • Ninth chunk: ./gradlew compileDebugKotlin compileDebugUnitTestKotlin: clean; unit tests BridgeEnvelopeCodecTest (63), BridgeEnvelopeCodecBilateralPayloadTest (1), SinglePathWebViewBridgeErrorResponseTest (2), SinglePathWebViewBridgeFuzzTest (5): 71 passed; the five repository gates and ci/conformance_evidence.py: pass.
  • Ninth chunk, device: the APK installed on the four phones; on the relaunched 5GN, DSM launches with no crash, the wallet renders its balances and identity through the rewritten codec, and logcat shows no request that failed to decode.
  • Tenth chunk: cargo check -p dsm --all-targets: clean; dsm lib state_machine (12 passed); cargo ndk … check: clean; ./gradlew compileDebugKotlin compileDebugUnitTestKotlin: clean; unit tests BridgeEnvelopeCodecTest (58), BridgeEnvelopeCodecBilateralPayloadTest (1), SinglePathWebViewBridgeErrorResponseTest (2), SinglePathWebViewBridgeFuzzTest (5): 66 passed; frontend related jest (84 suites, 497 tests), type-check, lint: pass; the five repository gates and ci/conformance_evidence.py: pass; make lint: passed.
  • Tenth chunk, device: the APK installed on the four phones; on the relaunched 5GN, DSM launches with no crash, the wallet renders its balances and identity, and logcat shows no request that failed to decode.

… never a substitute

The envelope builder re-derived the sender's public key under the state
lock and, when that failed, took the caller's copy or the persisted
app-state key with an empty default. The routes already resolve the key
from the signing authority fail-closed and the params check requires 64
bytes, so the builder embeds exactly that key or no evidence at all. The
receiver roots verification in its stored contact either way.
… not nothing in flight

counterparty_has_unconverged_inbound skipped any frozen half that did not
decode, so an unreadable inbound transfer read as absent and a send could
cross it. A half that does not decode is now an error, which the barrier
reports as a failed staging read.

Tests: an_unconverged_inbound_half_names_its_sender,
a_staged_half_that_does_not_decode_is_an_error_not_absence (the skip
restored -> red).
…mpty release

The reply-window sweep submitted whatever release bytes the row held, an
empty slice when it held none, which the sender refuses. A promoted row
without its post-admission release is logged as a defect and left
unmarked; nothing is submitted for it.
…ot the peer's with default fields

The route answered a BilateralPrepareResponse, the peer's message, with
every field but the commitment defaulted. It now answers a
BilateralTransferResponse: the prepare went out, transaction_hash names
the proposal's commitment. The frontend reads that answer, treats any
other payload as unexpected, and no longer decodes a reject the route
never produced; the tests stub the real answer.
Nothing in the page fetches an external host: the map the tile hosts served
is gone and the only fetch is a local sound. The proxy, the allowlist and
the test that locked it are deleted; the WebView serves APK assets and
handles everything else itself.
…CSP and service worker name no host the page uses

getHeaders answered a window global once it had been filled and never
asked the bridge again, so a changed identity was never seen. It now reads
the bridge on every call; the tests that reset the global are updated and
the cache test asserts the opposite property. The CSP's tile, maplibre,
dsm-wallet.io and localhost entries and the service worker's tile cache
had no consumer.
…t-level gate

BootstrapMeasurementReport envelopes (started, progress, finalize, aborted,
error) and the finalize path gated on a caller-supplied trust level
described as a C-DBRW entropy health test had no sender; C-DBRW is gone and
Genesis v2 (system.createGenesisV2) is the one bootstrap path. Such an
envelope now reaches the Core bridge and is refused as unsupported. Two
startup tests are renamed after the real op they exercise.
It fixes the identity pair to two 32-byte arrays and nothing else. The
unused domain-tag parameter and the doc's claims of hashing, canonization
and being the only object the Core trusts are gone.
…committed one

acceptOfflineTransfer emitted wallet.bilateralCommitted with committed: true
as soon as the accept answer arrived, before the peer's confirm. The event
is now wallet.bilateralAccepted and names the transfer (commitment and
counterparty) and nothing else; the transfer commits when the confirm
arrives as a BLE event. The toast it drives already said accepted.
…nted for a code nothing emits

The unwrapper rewrote codes 460, 404 and 408 into peer-connection, stale-
state and peer-timeout stories; the Kotlin dispatcher emits codes 1 to 8
and none of those. The message the bridge sent is shown with its code.
…t filled with false

Rust fills every nested status on every snapshot. The decoder answered
false, true and 'none' for anything absent and the store spread defaults
under whatever passed a five-field guard. The decoder now refuses a
snapshot without its lock or hardware status, reads the rest as sent, and
the store keeps the snapshot as decoded, adding only that it arrived.
…e; the response catch can release its entry

Any window message carrying a port replaced the bridge port, whenever it
arrived. The port is now taken once, from the empty-string message Kotlin
posts with it; a later port-bearing message, or one without the handshake,
is ignored and logged. In the port handler the response id was declared
inside the try, so the catch's cleanup never saw it and a response that
failed mid-decode held its pending entry until the timeout; the id is
declared outside.
The commit before last carried only the renamed provider test: its add
command failed on the old test path and staged nothing else. This is the
rest of the rename: the event map, the emitter, the accept path, the
provider's signal and the tests that name the event.
…ed protobuf classes

BridgeEnvelopeCodec carried its own varint and length-delimited parser with
hard-coded field numbers for every bridge message, beside the rule that
Kotlin implements no wire decoder. It now uses the generated classes of
dsm_app.proto for the request, the response, the error, the app-router,
preference and bilateral payloads and the envelope's error, keeping its
public shape. A request without a method is refused. Tests that asserted
the hand-rolled parser's private rules (a second oneof member, a wrong
wire type as a failure, an empty-versus-absent debug string) now assert
protobuf's semantics.
…y Rust's source tag, never by message text

The page recognised a safety refusal by a regex over any error message,
and Kotlin scanned every answer for an Envelope error with source tag 11,
which never parsed a framed answer and so never matched. Rust tags
DsmError::DeterministicSafety with source tag 11 and carries the class in
the error's context; the page now reads exactly that, at the two places
every transport error surfaces (decodeFramedEnvelopeV3 and the ingress
unwrapper). The regex, its callers and Kotlin's scan are gone.
…pendencies they carried

bluetooth, storage, threadsafe, jni, web-stack and formal gated no code in
the Core; three of them only pulled optional dependencies the Core never
used (jni, tokio-stream, rocksdb, axum, tower). Deleted with the build-info
function that only listed them and the build script's rustc and target
stamps it alone read. The lockfile loses rocksdb and its native build
chain and nothing else. sphincs-trace and bitcoin-testnet-bypass gate code
and stay.
…ule are gone

deterministic_id.rs produced hex UUID strings and had no caller.
calculate_next_entropy and hash_blake3 were reached only by their own
tests; init_crypto had no caller and was the only caller of the
init_sphincs self-test. All deleted with those tests; init_kyber stays,
key generation calls it.
@cryptskii
cryptskii merged commit 0f9ca26 into main Sep 27, 2026
17 of 21 checks passed
@cryptskii
cryptskii deleted the fix/placeholder-sweep-sdk-success-rows branch September 27, 2026 14:40
cryptskii added a commit that referenced this pull request Sep 27, 2026
- Rust gates (G1, red since #1035): `sofi::smt::fold::batch_fold` had no
  production caller; the verifier folds through `verify_batch`. The wrapper
  goes; the tests call the one fold over the economic tree's hashes.
- Rust tests (dsm_sdk): the offline-step fixture installed its host appliance
  into the process-wide factory and never removed it, so the offline-cash gate
  tests that run after it, which assert what a device with no appliance is
  refused, found one attached ("insufficient online balance"). `install` now
  returns a handle that uninstalls on drop.
- Android unit tests: `BridgeLoggerTest` asserted payload bytes in the bridge
  log, which #1038 removed on purpose (a payload can be the mnemonic). The two
  tests now assert the size is logged and no byte of the payload is.
- Android instrumented tests: `t64` asserted a 16-byte accept commitment was
  answered as success. Since #1039 the dispatcher answers the arm's error; the
  test asserts that error and its reason.

Reproduced first: the offline-step tests then the offline-cash tests in one
process fail exactly as CI does (2 failed, "insufficient online balance");
after the fix 27 passed. `dsm` lib sofi fold + validation: 59 passed.
`ci/sofi_reachability.py`: pass. `BridgeLoggerTest`: 10 passed. Android test
sources compile.
cryptskii added a commit that referenced this pull request Sep 27, 2026
…older schema fails it in its own words (#1041)

* fix(startup): a failed startup is the session's error, and a store at an older schema fails it in its own words

Both appliance phones sat on "STARTING RUNTIME / WARMING UP BRIDGE" with no
end: their stores were left at schema 24 by an older build, this build expects
25 and does not migrate, and nothing told the page. `initialize_sdk_core` set
SDK_READY false and returned the error to a caller that logged it, so the phase
stayed `runtime_loading`; Kotlin then logged "SDK initialized" after `initSdk`
answered false.

- A failed startup is recorded as the session's fatal error, so the phase is
  `error` and the page shows Rust's reason. A later successful startup takes
  back the failure it recorded; an error anyone else reported stays.
- Startup opens the client store. A store this build refuses fails startup in
  the store's words. Unopened, the refusal surfaced through whatever read the
  store first: on Android that was the sealed-seed read, which was downgraded to
  a warning and re-reported as "wallet seed not unlocked". A seed vault that
  cannot be read now fails init as itself.
- Kotlin logs a failed `initSdk` as a failure.
- `initSdkV3` had no caller and declared the SDK ready after setting only the
  storage directory. It goes with its declarations and `InitFailed`, which only
  it produced; Envelope field 31 is reserved.

Test, on the running fleet: a device created as wallet creation creates it, its
store left as schema 24 left it (stamped 24, without the table 25 added), the
process restarted; startup fails with SCHEMA RESET REQUIRED, the session's
phase is `error` with that message, and after the wipe the refusal names,
startup succeeds and the error is gone. Mutations: the recording dropped, then
the store open dropped; each turns the test red.

* fix(ci): the four jobs red on main since the sweep merges

- Rust gates (G1, red since #1035): `sofi::smt::fold::batch_fold` had no
  production caller; the verifier folds through `verify_batch`. The wrapper
  goes; the tests call the one fold over the economic tree's hashes.
- Rust tests (dsm_sdk): the offline-step fixture installed its host appliance
  into the process-wide factory and never removed it, so the offline-cash gate
  tests that run after it, which assert what a device with no appliance is
  refused, found one attached ("insufficient online balance"). `install` now
  returns a handle that uninstalls on drop.
- Android unit tests: `BridgeLoggerTest` asserted payload bytes in the bridge
  log, which #1038 removed on purpose (a payload can be the mnemonic). The two
  tests now assert the size is logged and no byte of the payload is.
- Android instrumented tests: `t64` asserted a 16-byte accept commitment was
  answered as success. Since #1039 the dispatcher answers the arm's error; the
  test asserts that error and its reason.

Reproduced first: the offline-step tests then the offline-cash tests in one
process fail exactly as CI does (2 failed, "insufficient online balance");
after the fix 27 passed. `dsm` lib sofi fold + validation: 59 passed.
`ci/sofi_reachability.py`: pass. `BridgeLoggerTest`: 10 passed. Android test
sources compile.
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