mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 07:45:20 +00:00
Gateway mode: opt-in mesh↔Nostr uplink for geohash channels (#1384)
* Add capability bits to announce TLV Announces now carry an optional capabilities TLV (0x05): a little-endian bitfield with named bits for upcoming features (prekeys, wifiBulk, gateway, groups, board, vouch, meshDiagnostics). Old clients skip the unknown TLV; peers without it decode as nil so features can distinguish "legacy peer" from "advertises nothing". PeerCapabilities lives in BitFoundation with a minimal-length encoding that preserves unknown bits for forward compatibility. Peer capabilities are stored in the BLE peer registry on verified announce and exposed via BLEService.peerCapabilities(_:). The local advertisement set is empty until each feature ships its bit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Gateway mode: opt-in mesh↔Nostr uplink for geohash channels An opt-in "internet gateway" toggle lets one connected phone bridge the local geohash channel for mesh-only peers: signed kind-20000 events ride a new nostrCarrier (0x28) packet — directed to the gateway for uplink, broadcast with TTL for downlink — with Schnorr verification at every hop, CourierStore-style quotas, and explicit loop-prevention rules. - BitFoundation: MessageType.nostrCarrier = 0x28 - NostrCarrierPacket: 2-byte-length TLV codec (direction, geohash, signed event JSON), 16 KiB cap, tolerant decoder - GatewayService: closure-injected policy layer — verify gates (sig, kind, #g tag, age, size), uplink quotas (10/min/depositor rate limit, offline queue of 20 total / 5 per depositor, drop-oldest, flush on reconnect), downlink budget (30/min, bounded drop-oldest backlog), bounded loop-prevention ID sets - BLEService: runtime capability bits (advertise .gateway only while the toggle is on, re-announce on change), signed directed uplink sends, carrier ingress with depositor signature verification - Mesh-only senders uplink automatically from sendGeohash when no relay is connected and a reachable peer advertises .gateway; once-per- channel "sent via mesh gateway" notice - UI: gateway toggle beside the Tor toggle, globe header indicator, VoiceOver labels, xcstrings entries Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Gateway: harden downlink freshness, uplink verify ordering, and drain Fixes the confirmed downlink/uplink defects from the PR #1384 review + Codex findings: - Downlink age + #g gate (Codex P2 / review #1): rebroadcastRelayEvent now drops events outside the same freshness window receivers enforce and whose #g tag mismatches the carrier geohash, BEFORE spending any budget — so a 1h/200-event channel-resubscribe backfill no longer burns the 30/min BLE budget on events every receiver drops. - Rate-limit + dedup before Schnorr (review #2): handleUplinkDeposit now runs cheap structural checks + carried-ID dedup + rate-token consume before isValidSignature(), so a replay flood is bounded by cheap work instead of unbounded main-actor verifies. - Quota-dropped deposits not rendered (review #3): enqueueUplink reports acceptance and injectInbound only fires for events actually published/queued, ending the local-timeline divergence. - Drain timer + mark-after-send (Codex P2 / review #4): a burst beyond budget now arms a timer to drain when the window frees; rebroadcast IDs are marked only after an event is actually sent, so overflow- dropped events stay retryable. - Symmetric publish path (review #5): the gateway publish closure now refuses when no geo relay is known, matching the local send path instead of publishing dead traffic to default relays. - Loop-rule doc (review #7): softened to reflect that rule 3 is a call-site convention with unit-tested backstops; added tests for the publishedEventIDs backstop, downlink freshness/mismatch, drain timer, and quota-drop non-injection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Gateway: stop self-echo of uplinked events onto the mesh Every event a gateway uplinks to the relays comes back through its own geohash subscription. `rebroadcastRelayEvent` deduped against `meshBroadcastEventIDs`, `rebroadcastEventIDs`, and `pendingDownlinks`, but not `publishedEventIDs` — so an event this gateway just published was downlink-rebroadcast onto the same mesh it originated from, doubling BLE airtime per uplinked message and able to starve the 30/min downlink budget on a busy channel (device-confirmed, filed on #1384). Fix: also skip the downlink rebroadcast when the event id is in `publishedEventIDs`. That set is already the bounded (drop-oldest, capacity maxTrackedEventIDs) loop-rule-2 uplink cache, populated only by `publish()`, so genuine inbound-from-internet events (never published here) still rebroadcast normally. Reconciles cleanly with the existing loop-prevention sets — no new state. Adds a GatewayServiceTests case asserting an uplinked event that echoes back via the subscription is not rebroadcast, while a genuine inbound event still is. 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>
This commit is contained in:
co-authored by
jack
Claude Fable 5
parent
2360140760
commit
d4f0c49787
@@ -72,6 +72,7 @@ final class ChatViewModelBootstrapper {
|
||||
configureNoiseCallbacks()
|
||||
bindTransferProgress()
|
||||
configureGeoChannels()
|
||||
configureGateway()
|
||||
bindTeleportState()
|
||||
requestNotifications()
|
||||
registerObservers()
|
||||
@@ -244,6 +245,72 @@ private extension ChatViewModelBootstrapper {
|
||||
)
|
||||
}
|
||||
|
||||
/// Wires the gateway-mode policy layer (`GatewayService`) to the mesh
|
||||
/// transport, the relay manager, and the inbound Nostr pipeline. All
|
||||
/// dependencies are closures so the service stays unit-testable with
|
||||
/// fakes.
|
||||
func configureGateway() {
|
||||
// Gateway mode bridges BLE mesh <-> Nostr; a mock transport (tests)
|
||||
// has no carrier packets to bridge.
|
||||
guard let bleService = viewModel.meshService as? BLEService else { return }
|
||||
let gateway = GatewayService.shared
|
||||
|
||||
gateway.publishToRelays = { event, geohash in
|
||||
let relays = GeoRelayDirectory.shared.closestRelays(
|
||||
toGeohash: geohash,
|
||||
count: TransportConfig.nostrGeoRelayCount
|
||||
)
|
||||
// Symmetric with the local send path (GeohashSubscriptionManager
|
||||
// .sendGeohash): with no known geo relay, refuse rather than
|
||||
// publish to default relays no geo subscriber reads — that would
|
||||
// be silent dead traffic, not delivery.
|
||||
guard !relays.isEmpty else {
|
||||
SecureLogger.warning("🌐 Gateway: no geo relays for #\(geohash); not publishing carried event", category: .session)
|
||||
return
|
||||
}
|
||||
NostrRelayManager.shared.sendEvent(event, to: relays)
|
||||
}
|
||||
gateway.broadcastToMesh = { [weak bleService] payload in
|
||||
bleService?.broadcastNostrCarrier(payload)
|
||||
}
|
||||
gateway.sendToGatewayPeer = { [weak bleService] payload, peer in
|
||||
bleService?.sendNostrCarrier(payload, to: peer) ?? false
|
||||
}
|
||||
gateway.availableGatewayPeers = { [weak bleService] in
|
||||
bleService?.reachableGatewayPeers() ?? []
|
||||
}
|
||||
gateway.relaysConnected = { NostrRelayManager.shared.isConnected }
|
||||
gateway.currentGeohash = { [weak viewModel] in viewModel?.currentGeohash }
|
||||
// Carried events enter the same pipeline as relay-received events so
|
||||
// blocking, rate limits, dedup, and rendering behave identically.
|
||||
gateway.injectInbound = { [weak viewModel] event in
|
||||
viewModel?.handleNostrEvent(event)
|
||||
}
|
||||
// The capability bit is advertised ONLY while the toggle is on; a
|
||||
// change forces a re-announce so peers learn promptly.
|
||||
gateway.onEnabledChanged = { [weak bleService] enabled in
|
||||
bleService?.setLocalCapability(.gateway, enabled: enabled)
|
||||
}
|
||||
bleService.onNostrCarrierPacket = { payload, from, directedToUs in
|
||||
GatewayService.shared.handleMeshCarrier(payload, from: from, directedToUs: directedToUs)
|
||||
}
|
||||
|
||||
// Uplinks deposited while relays were unreachable flush on reconnect.
|
||||
NostrRelayManager.shared.$isConnected
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { connected in
|
||||
if connected {
|
||||
GatewayService.shared.flushQueuedUplinks()
|
||||
}
|
||||
}
|
||||
.store(in: &viewModel.cancellables)
|
||||
|
||||
// Apply the persisted toggle at launch.
|
||||
if gateway.isEnabled {
|
||||
bleService.setLocalCapability(.gateway, enabled: true)
|
||||
}
|
||||
}
|
||||
|
||||
func bindTeleportState() {
|
||||
viewModel.locationManager.$teleported
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
||||
Reference in New Issue
Block a user