SYNC_REQUEST 2 (#616)

* wip

* woohooo

* Plumtree gossip: don't subset REQUEST_SYNC fanout; make RequestSyncPacket.encode use const

* bloom -> gcs [wip]

* fix build

* fix broadcast

* prune old messages too

* faster sync

* prune better

* adjust parameters

* fix(sync): make cap a constant in GCSFilter.buildFilter to silence 'never mutated' warning

* fix(mesh): surface self-origin public messages recovered via sync; only ignore self when TTL != 0 in handleMessage

* sync: allow self messages via GCS restore and relax TTL==0 acceptance\n- Bypass dedup for self TTL==0 packets in handleReceivedPacket\n- Accept self TTL==0 in handleMessage and set nickname\n- Accept unknown senders for TTL==0 with anon# prefix to restore history

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
callebtc
2025-09-15 13:57:09 +02:00
committed by GitHub
co-authored by jack
parent 42cdc4c123
commit 2ac01db9c4
6 changed files with 568 additions and 7 deletions
+99 -7
View File
@@ -138,6 +138,9 @@ final class BLEService: NSObject {
private var pendingDirectedRelays: [String: [String: (packet: BitchatPacket, enqueuedAt: Date)]] = [:]
// Debounce for 'reconnected' logs
private var lastReconnectLogAt: [String: Date] = [:]
// MARK: - Gossip Sync
private var gossipSyncManager: GossipSyncManager?
// MARK: - Maintenance Timer
@@ -398,9 +401,15 @@ final class BLEService: NSObject {
}
timer.resume()
maintenanceTimer = timer
// Publish initial empty state
requestPeerDataPublish()
// Initialize gossip sync manager
let sync = GossipSyncManager(myPeerID: myPeerID)
sync.delegate = self
sync.start()
self.gossipSyncManager = sync
}
func setNickname(_ nickname: String) {
@@ -775,6 +784,8 @@ final class BLEService: NSObject {
self.messageDeduplicator.markProcessed(dedupID)
// Call synchronously since we're already on background queue
self.broadcastPacket(signedPacket)
// Track our own broadcast for sync
self.gossipSyncManager?.onPublicPacketSeen(signedPacket)
}
}
}
@@ -1099,10 +1110,13 @@ final class BLEService: NSObject {
}
// For broadcast (no directed peer) and non-fragment, choose a subset deterministically
// Special-case announces: do NOT subset to maximize reach for presence
// Special-case control/presence messages: do NOT subset to maximize immediate coverage
var selectedPeripheralIDs = Set(allowedPeripheralIDs)
var selectedCentralIDs = Set(allowedCentralIDs)
if directedOnlyPeer == nil && packet.type != MessageType.fragment.rawValue && packet.type != MessageType.announce.rawValue {
if directedOnlyPeer == nil
&& packet.type != MessageType.fragment.rawValue
&& packet.type != MessageType.announce.rawValue
&& packet.type != MessageType.requestSync.rawValue {
let kp = subsetSizeForFanout(allowedPeripheralIDs.count)
let kc = subsetSizeForFanout(allowedCentralIDs.count)
selectedPeripheralIDs = selectDeterministicSubset(ids: allowedPeripheralIDs, k: kp, seed: messageID)
@@ -1133,6 +1147,12 @@ final class BLEService: NSObject {
}
}
// Directed send helper (unicast to a specific peerID) without altering packet contents
private func sendPacketDirected(_ packet: BitchatPacket, to peerID: String) {
guard let data = packet.toBinaryData(padding: false) else { return }
sendOnAllLinks(packet: packet, data: data, pad: false, directedOnlyPeer: peerID)
}
// MARK: - Directed store-and-forward
private func spoolDirectedPacket(_ packet: BitchatPacket, recipientPeerID: String) {
let msgID = makeMessageID(for: packet)
@@ -1339,7 +1359,10 @@ final class BLEService: NSObject {
// Efficient deduplication
// Important: do not dedup fragment packets globally (each piece must pass)
if packet.type != MessageType.fragment.rawValue && messageDeduplicator.isDuplicate(messageID) {
// Special case: allow our own packets recovered via sync (TTL==0) to pass
// through even if we've marked them as seen at send time.
let allowSelfSyncReplay = (packet.ttl == 0) && (senderID == myPeerID)
if packet.type != MessageType.fragment.rawValue && !allowSelfSyncReplay && messageDeduplicator.isDuplicate(messageID) {
// Announce packets (type 1) are sent every 10 seconds for peer discovery
// It's normal to see these as duplicates - don't log them to reduce noise
if packet.type != MessageType.announce.rawValue {
@@ -1383,6 +1406,9 @@ final class BLEService: NSObject {
case .message:
handleMessage(packet, from: senderID)
case .requestSync:
handleRequestSync(packet, from: senderID)
case .noiseHandshake:
handleNoiseHandshake(packet, from: senderID)
@@ -1583,12 +1609,17 @@ final class BLEService: NSObject {
// Only notify of connection for new or reconnected peers when it is a direct announce
if (packet.ttl == self.messageTTL) && (isNewPeer || isReconnectedPeer) {
self.delegate?.didConnectToPeer(peerID)
// Schedule initial unicast sync to this peer
self.gossipSyncManager?.scheduleInitialSyncToPeer(peerID, delaySeconds: 1.0)
}
self.requestPeerDataPublish()
self.delegate?.didUpdatePeerList(currentPeerIDs)
}
// Track for sync (include our own and others' announces)
gossipSyncManager?.onPublicPacketSeen(packet)
// Send announce back for bidirectional discovery (only once per peer)
let announceBackID = "announce-back-\(peerID)"
let shouldSendBack = !messageDeduplicator.contains(announceBackID)
@@ -1610,17 +1641,33 @@ final class BLEService: NSObject {
}
}
}
// Handle REQUEST_SYNC: decode payload and respond with missing packets via sync manager
private func handleRequestSync(_ packet: BitchatPacket, from peerID: String) {
guard let req = RequestSyncPacket.decode(from: packet.payload) else {
SecureLogger.warning("⚠️ Malformed REQUEST_SYNC from \(peerID)", category: .session)
return
}
gossipSyncManager?.handleRequestSync(fromPeerID: peerID, request: req)
}
// Mention parsing moved to ChatViewModel
private func handleMessage(_ packet: BitchatPacket, from peerID: String) {
// Ignore self-origin public messages that may be seen again via relay
if peerID == myPeerID { return }
// Ignore self-origin public messages except when returned via sync (TTL==0).
// This allows our own messages to be surfaced when they come back via
// the sync path without re-processing regular relayed copies.
if peerID == myPeerID && packet.ttl != 0 { return }
var accepted = false
var senderNickname: String = ""
if let info = peers[peerID], info.isVerifiedNickname {
// If the packet is from ourselves (e.g., recovered via sync TTL==0), accept immediately
if peerID == myPeerID {
accepted = true
senderNickname = myNickname
}
else if let info = peers[peerID], info.isVerifiedNickname {
// Known verified peer path
accepted = true
senderNickname = info.nickname
@@ -1648,6 +1695,22 @@ final class BLEService: NSObject {
}
}
}
// If still not accepted and this is a sync-returned packet (TTL==0),
// accept with a generic nickname so history can be restored even for
// peers we haven't verified yet.
if !accepted && packet.ttl == 0 {
accepted = true
senderNickname = "anon" + String(peerID.prefix(4))
}
}
// Track broadcast messages for sync (treat nil or 0xFF..0xFF as broadcast)
let isBroadcastRecipient: Bool = {
guard let r = packet.recipientID else { return true }
return r.count == 8 && r.allSatisfy { $0 == 0xFF }
}()
if isBroadcastRecipient && packet.type == MessageType.message.rawValue {
gossipSyncManager?.onPublicPacketSeen(packet)
}
guard accepted else {
@@ -1780,6 +1843,8 @@ final class BLEService: NSObject {
// Remove the peer when they leave
peers.removeValue(forKey: peerID)
}
// Remove any stored announcement for sync purposes
gossipSyncManager?.removeAnnouncementForPeer(peerID)
// Send on main thread
notifyUI { [weak self] in
guard let self = self else { return }
@@ -1861,6 +1926,8 @@ final class BLEService: NSObject {
self?.broadcastPacket(signedPacket)
}
}
// Ensure our own announce is included in sync state
gossipSyncManager?.onPublicPacketSeen(signedPacket)
}
func sendDeliveryAck(for messageID: String, to peerID: String) {
@@ -2088,6 +2155,8 @@ final class BLEService: NSObject {
if !peer.isConnected {
if age > retention {
SecureLogger.debug("🗑️ Removing stale peer after reachability window: \(peerID) (\(peer.nickname))", category: .session)
// Also remove any stored announcement from sync candidates
gossipSyncManager?.removeAnnouncementForPeer(peerID)
peers.removeValue(forKey: peerID)
removedOfflineCount += 1
}
@@ -2249,6 +2318,29 @@ final class BLEService: NSObject {
}
}
// MARK: - GossipSyncManager Delegate
extension BLEService: GossipSyncManager.Delegate {
func sendPacket(_ packet: BitchatPacket) {
if DispatchQueue.getSpecific(key: messageQueueKey) != nil {
broadcastPacket(packet)
} else {
messageQueue.async { [weak self] in self?.broadcastPacket(packet) }
}
}
func sendPacket(to peerID: String, packet: BitchatPacket) {
if DispatchQueue.getSpecific(key: messageQueueKey) != nil {
sendPacketDirected(packet, to: peerID)
} else {
messageQueue.async { [weak self] in self?.sendPacketDirected(packet, to: peerID) }
}
}
func signPacketForBroadcast(_ packet: BitchatPacket) -> BitchatPacket {
return noiseService.signPacket(packet) ?? packet
}
}
// MARK: - CBCentralManagerDelegate
extension BLEService: CBCentralManagerDelegate {