From 1fd2da18f5d5967b54e6ac88827a7bf033180e09 Mon Sep 17 00:00:00 2001 From: jack <212554440+jackjackbits@users.noreply.github.com> Date: Tue, 23 Sep 2025 21:09:22 +0200 Subject: [PATCH] Improve BLE relay reliability (#665) Co-authored-by: jack --- bitchat/Services/BLEService.swift | 15 ++++++++-- bitchat/Services/RelayController.swift | 39 +++++++++++--------------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/bitchat/Services/BLEService.swift b/bitchat/Services/BLEService.swift index a54bd43c..55c2b490 100644 --- a/bitchat/Services/BLEService.swift +++ b/bitchat/Services/BLEService.swift @@ -264,7 +264,9 @@ final class BLEService: NSObject { // MARK: - Helpers: IDs, selection, and write backpressure private func makeMessageID(for packet: BitchatPacket) -> String { let senderID = packet.senderID.hexEncodedString() - return "\(senderID)-\(packet.timestamp)-\(packet.type)" + let digest = SHA256.hash(data: packet.payload) + let digestPrefix = digest.prefix(4).map { String(format: "%02x", $0) }.joined() + return "\(senderID)-\(packet.timestamp)-\(packet.type)-\(digestPrefix)" } private func subsetSizeForFanout(_ n: Int) -> Int { @@ -1183,6 +1185,13 @@ final class BLEService: NSObject { // Determine last-hop link for this message to avoid echoing back let messageID = makeMessageID(for: packet) let ingressLink: LinkID? = collectionsQueue.sync { ingressByMessageID[messageID]?.link } + let directedPeerHint: String? = { + if let explicit = directedOnlyPeer { return explicit } + if let recipient = packet.recipientID?.hexEncodedString(), !recipient.isEmpty { + return recipient + } + return nil + }() let states = snapshotPeripheralStates() var minCentralWriteLen: Int? @@ -1236,7 +1245,7 @@ final class BLEService: NSObject { // Special-case control/presence messages: do NOT subset to maximize immediate coverage var selectedPeripheralIDs = Set(allowedPeripheralIDs) var selectedCentralIDs = Set(allowedCentralIDs) - if directedOnlyPeer == nil + if directedPeerHint == nil && packet.type != MessageType.fragment.rawValue && packet.type != MessageType.announce.rawValue && packet.type != MessageType.requestSync.rawValue { @@ -1247,7 +1256,7 @@ final class BLEService: NSObject { } // If directed and we currently have no links to forward on, spool for a short window - if let only = directedOnlyPeer, + if let only = directedPeerHint, selectedPeripheralIDs.isEmpty && selectedCentralIDs.isEmpty, (packet.type == MessageType.noiseEncrypted.rawValue || packet.type == MessageType.noiseHandshake.rawValue) { spoolDirectedPacket(packet, recipientPeerID: only) diff --git a/bitchat/Services/RelayController.swift b/bitchat/Services/RelayController.swift index 45291c66..f600eb61 100644 --- a/bitchat/Services/RelayController.swift +++ b/bitchat/Services/RelayController.swift @@ -18,13 +18,17 @@ struct RelayController { isAnnounce: Bool, degree: Int, highDegreeThreshold: Int) -> RelayDecision { + let ttlCap = min(ttl, TransportConfig.messageTTLDefault) + // Suppress obvious non-relays - if ttl <= 1 || senderIsSelf { return RelayDecision(shouldRelay: false, newTTL: ttl, delayMs: 0) } + if ttlCap <= 1 || senderIsSelf { + return RelayDecision(shouldRelay: false, newTTL: ttlCap, delayMs: 0) + } // For session-critical or directed traffic, be deterministic and reliable if isHandshake || isDirectedFragment || isDirectedEncrypted { // Always relay with no TTL cap for these types - let newTTL = (ttl &- 1) + let newTTL = ttlCap &- 1 // Slight jitter to desynchronize without adding too much latency // Tighter for faster multi-hop handshakes and directed DMs let delayRange: ClosedRange = isHandshake ? 10...35 : 20...60 @@ -32,28 +36,17 @@ struct RelayController { return RelayDecision(shouldRelay: true, newTTL: newTTL, delayMs: delayMs) } - // Degree-aware probability to reduce floods in dense graphs (broadcast/public) - let baseProb: Double - switch degree { - case 0...2: baseProb = 1.0 - case 3...4: baseProb = 0.9 - case 5...6: baseProb = 0.7 - case 7...9: baseProb = 0.55 - default: baseProb = 0.45 - } - let prob = baseProb - let shouldRelay = Double.random(in: 0...1) <= prob - // TTL clamping for broadcast - // - Dense graphs: keep very low to avoid floods - // - Sparse graphs: allow slightly longer reach for multi-hop discovery - // - Announces in sparse graphs get a bit more headroom - let ttlCap: UInt8 = { - if degree >= highDegreeThreshold { return 3 } - return isAnnounce ? 7 : 6 + // - Dense graphs: keep lower but still allow multi-hop bridging + // - Announces get a bit more headroom + let ttlLimit: UInt8 = { + if degree >= highDegreeThreshold { + return max(UInt8(2), min(ttlCap, UInt8(5))) + } + let preferred = UInt8(isAnnounce ? 7 : 6) + return max(UInt8(2), min(ttlCap, preferred)) }() - let clamped = max(1, min(ttl, ttlCap)) - let newTTL = clamped &- 1 + let newTTL = ttlLimit &- 1 // Wider jitter window to allow duplicate suppression to win more often // For sparse graphs (<=2), relay quickly to avoid cancellation races @@ -64,6 +57,6 @@ struct RelayController { case 6...9: delayMs = Int.random(in: 80...180) default: delayMs = Int.random(in: 100...220) } - return RelayDecision(shouldRelay: shouldRelay, newTTL: newTTL, delayMs: delayMs) + return RelayDecision(shouldRelay: true, newTTL: newTTL, delayMs: delayMs) } }