Every inbound relay event was Schnorr-verified TWICE, both times on the
main actor: once in NostrRelayManager.handleParsedMessage and again in
each NostrInboundPipeline / GeoPresenceTracker handler. Each
verification re-serializes the event JSON, hashes it, and runs a
secp256k1 Schnorr verify, so busy geohashes paid double crypto on the
UI thread. Geohash gift-wrap NIP-17 decryption (two ECDH+ChaCha layers)
also ran on the main actor.
Changes:
- NostrRelayManager now owns a serial off-main inbound pipeline
(AsyncStream + single consumer task): frames are parsed and verified
in arrival order off the main actor, preserving per-subscription
delivery order. This is the single verification point for the whole
inbound path; downstream handlers only ever see verified events.
- Dedup stays two-phase and unpoisonable: a cheap main-actor duplicate
LOOKUP runs before verification (duplicate fan-in from several relays
never pays for crypto — previously every duplicate was verified), and
events are RECORDED as seen only after the signature verifies, so a
forged-signature copy can never suppress the genuine event.
- NostrInboundPipeline and GeoPresenceTracker drop their redundant
re-verification; geohash gift-wrap decryption moves off the main
actor following the existing account-mailbox Task.detached pattern
(atomic main-actor check-and-record, then decrypt off-main, then hop
back for state updates).
- Tests: relay-level coverage for tampered gift wraps and for in-order
delivery of back-to-back frames; pipeline-level tampered-signature
tests move to the relay boundary where the invariant now lives; the
mock relay connection queues frames emitted before receive re-arms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BLEService's per-packet-type orchestration moves into owned, tested
components (BLEAnnounceHandler, BLEPublicMessageHandler,
BLENoisePacketHandler, BLEFileTransferHandler, BLEFragmentHandler),
each taking an environment struct of closures so every queue hop stays
in BLEService and the handlers are synchronously testable. Behavior is
preserved verbatim, including Noise session recovery on decrypt failure
and single-block UI event ordering. handleLeave/handleRequestSync stay
in place as already-thin delegations. BLEService drops to 3393 lines.
Four coordinators (delivery, private conversation, Nostr, public
conversation) drop their unowned/weak ChatViewModel back-references for
narrow @MainActor context protocols, with ChatViewModel conformances as
single shared witnesses for overlapping members. Their true coupling is
now an explicit, reviewable surface, and each gains a mock-context test
suite covering flows previously testable only through the full view
model. Delivery/read acks now also clear the router's retained-send
outbox via the delivery context.
New LargeTopologyTests exercise production-shaped meshes with the
in-memory harness: an 8-peer relay chain with per-hop TTL decay, a
14-peer cyclic mesh with exactly-once delivery, partition/heal, and
topology churn.
App-layer runtime/model files updated alongside.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>