Commit Graph
4 Commits
Author SHA1 Message Date
b205a55523 Consolidate duplicate BLE links after restore; duplicate fragment-stream suppression; onChange coalescing (#1426)
* Consolidate duplicate BLE links after restore; suppress duplicate fragment streams; onChange coalescing

Field evidence (two-phone test after BLE state-restoration relaunches):
both phones held 2-3 simultaneous same-role connections to each other —
one side received every packet from three distinct centrals all bound to
the same peer — so every PTT voice frame arrived 3x, and a 41KB voice
file went out as TWO complete independent 89-fragment streams (different
assembly ids), both fully reassembled and the duplicate only dropped at
the very end by messageID dedup. 2-3x airtime/battery on all traffic
plus doubled reassembly memory.

Root causes and fixes:

- Duplicate links stayed unbound forever: announces (the only packet
  that binds a link to a peer) went through the per-peer duplicate-link
  collapse, so a peer's second/third link never received the announce it
  needed to become bound — and unbound links pass the collapse untouched,
  so every broadcast sprayed down all of them. Direct announces now
  bypass the collapse and reach every live link (relayed announces keep
  it); once bound, the existing collapse dedups all traffic.

- Same-role link retirement: a verified direct announce now consolidates
  our central-role connections to that peer — keep the link the announce
  arrived on (or the most recently bound one), cancel other connected
  links bound to the same peer. One connection per role per peer is the
  normal dual-role topology; only same-role duplicates are touched, only
  links already announce-bound are retired (never pre-announce links),
  at most one retirement per peer per rebind-cooldown window, and the
  peer keeps a live link either way. Directness stays forgeable (TTL is
  unsigned), so a replay could nominate the survivor — bounded by the
  cooldown and by DM routing's existing canDeliverSecurely gate. A
  rotation rebind now also cancels other stale links still bound to the
  rotated-away ID, so the ghost identity retires promptly.

- Deterministic preferred-link collapse: when several bound links to one
  peer are candidates, collapse now keeps the peer's most recently bound
  link (the reverse-mapped one) instead of dictionary order; links
  without a discovered characteristic are excluded from fanout (they
  cannot be written to, and could silently eat a peer's collapsed copy).
  links(to:) now reports all bound peripheral links, and removing one
  duplicate no longer clobbers the reverse map of the survivor.

- Duplicate fragment streams: a transferId-less resend (gossip-sync
  replay, spool) of file content already being fragmented out to a
  covering audience is dropped at the outbound scheduler (broadcast
  covers everyone; directed covers its recipient). App-initiated sends
  carry an explicit transferId the progress UI tracks and always run.
  A peer that asks after the stream completes still gets a resend.

- didUnsubscribeFrom no longer flaps a peer that is still live on other
  links (the far side retiring its duplicate arrives as an unsubscribe);
  didDisconnectPeripheral bookkeeping is skipped for self-retired links
  by removing the store entry before cancelling.

Also: guard the DeliveryStatus onChange state write in TextMessageView/
MediaMessageView (unconditional per-row writes under a message storm
tripped SwiftUI's "tried to update multiple times per frame" warning).

The restore-path bgRemaining=∞ log was already fixed on main (#1425
review follow-up 07b5ac31: init-time seed + sampler-routed restore
captures); verified, no change needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Review fixes: multi-link disconnect guard, characteristic-aware retirement

Adversarial review of the duplicate-link PR (merge-with-nits):

- didDisconnectPeripheral gets the same multi-link guard as the
  unsubscribe path: when a duplicate link drops naturally while the peer
  stays live on another (dual-role central link, or a second bound link
  during the post-restore consolidation window), peer-disconnect
  bookkeeping (markDisconnected + disconnect notify) no longer runs — a
  UI blip until the next announce re-marked the peer connected. The
  reverse map was just repaired onto a connected survivor, so
  directLinkState is the accurate probe. Scan restart and connect-slot
  refill stay unguarded: they respond to the physical drop regardless of
  remaining logical links. (No unit test: driving the CBCentralManager
  delegate requires CBPeripheral instances, which cannot be constructed
  in tests; the policy pieces backing the guard are covered.)

- Retirement is now characteristic-aware: the policy snapshot carries
  characteristic presence, and keptPeripheralUUID selects anchors only
  among writable links while any exist — consolidation must not keep a
  link mid-service-rediscovery (didModifyServices cleared its
  characteristic) and cancel the writable duplicate, stranding outbound
  traffic on the central link until rediscovery finishes. When neither
  anchor is writable but a writable duplicate exists, consolidation
  defers to a later announce instead of guessing. The reverse-map
  survivor repair in removePeripheral prefers writable links for the
  same reason. Three new policy tests cover charless-vs-writable.

Deferred with code comments per review: central-side collapse keeps the
oldest subscription (no recency signal; remote consolidates within its
cooldown), and the extra preferred-bindings bleQueue hop in
sendOnAllLinks (fold into a combined snapshot if profiling flags it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 19:35:18 +02:00
3e9230229d Heal peer-ID rotation: rebind link on verified announce, retire ghost peer (#1401)
* Heal peer-ID rotation: rebind link on verified announce, retire ghost

When a peer relaunches it rotates its ephemeral peer ID, but a
still-open BLE connection kept its stale peripheral/central→peerID
binding for the reachability retention window (~45-60s). Until it aged
out, the other side showed a duplicate ghost peer and dropped the
rotated peer's direct announces as spoofing attempts.

Root cause: the ingress guard rejected a direct announce whose claimed
sender differed from the link binding before signature verification
could ever see it, so the binding could never heal.

- Ingress guard: let a mismatched direct announce through, attributed
  to the claimed sender; REQUEST_SYNC keeps the strict binding check.
- Bind sites: raw (pre-verification) announces may only bind unbound
  links, never rebind bound ones — rebinding now requires the announce
  handler's signature verification to pass.
- On a verified direct announce whose ingress link is bound to a
  different peer, rebind the link to the announced ID and retire the
  rotated-away ID immediately (registry + gossip + UI), mirroring
  handleLeave.
- BLELinkStateStore.bindPeripheral: drop the previous peer's reverse
  mapping on rebind so the retired ID no longer claims the link.

Fixes #1387

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Contain forged-directness rebinds: no identity steal, per-link cooldown

The announce signature does not authenticate directness (TTL is
excluded from signing because relays mutate it), so a bound peer could
replay another peer's fresh signed announce with its TTL restored and
reach the rotation rebind path. Contain what such a replay can do:

- Refuse a rebind when the claimed identity already owns another live
  link — a replayed announce can no longer steal a connected peer's
  binding or route its directed traffic to the replaying link.
- Allow at most one rebind per link per cooldown window (60s) so two
  identities cannot fight over a link in a replay flip-flop, with each
  flip retiring the other peer.

A replay against an identity with no live link remains possible, but
that capability already exists today on unbound links, where any raw
direct announce binds pre-verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 10:08:23 +02:00
jackandClaude Fable 5 6bda919dd4 Fix transport reliability gaps: Tor stalls, weak-signal sends, GCS input validation
NostrRelayManager no longer strands work when Tor is slow to bootstrap:
failed readiness waits retry (bounded by nostrTorReadyMaxWaitAttempts)
instead of dropping queued relay connections, parked EOSE callbacks fire
after exhaustion so callers never hang, and sends made before Tor is
ready are queued locally (capped) instead of being dropped on a failed
wait - still strictly fail-closed.

MessageRouter now prefers a connected transport over a merely
window-reachable one, and sends made on a weak reachability signal are
retained in the outbox until a delivery/read ack confirms receipt
(receivers dedup by message ID), with resends bounded by attempt count.

GCS sync filters from the wire are bounds-checked (p in 1...32, m > 1)
at both the packet decode and filter decode layers; oversized Golomb
parameters previously decoded to garbage via silent shift overflow.

BLELinkStateStore is now explicitly pinned to bleQueue: debug builds
trap any access from another queue, enforcing the ownership discipline
the surrounding code already relied on by convention.

CI gains an iOS simulator build job (arm64 only; the vendored Arti
xcframework has no x86_64 simulator slice) so iOS-conditional code
paths are compile-checked - SPM tests only cover the macOS slice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 16:22:35 +01:00
ffa0d7aa4f [codex] Extract BLE link state store (#1310)
* Refactor BLE transport event handling

* Make image output paths unique

* Keep queued Nostr read receipts alive

* Refine BLE ingress fanout

* Rediscover BLE service after invalidation

* Extract BLE notification retry buffer

* Extract BLE inbound write buffer

* Extract BLE fragment assembly buffer

* Tidy secure log handling from device run

* Extract BLE outbound fragment scheduler

* Harden app CI media tests

* Redact BLE message content from logs

* Extract BLE Noise session queues

* Fix BLE read receipt UI updates

* Allow self-authored RSR ingress replies

* Harden read receipt queue test timing

* Extract BLE outbound policy and incoming file storage

* Avoid duplicate BLE link snapshots during send

* Canonicalize Nostr relay URLs

* Extract BLE link state store

* Extract BLE connection scheduler

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
2026-05-31 14:24:09 +02:00