diff --git a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt index 2a0624f9..98bb7fd8 100644 --- a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt @@ -210,6 +210,14 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro val peerID = routed.peerID ?: "unknown" if (peerID == myPeerID) return false + + // Ignore stale announcements older than STALE_PEER_TIMEOUT + val now = System.currentTimeMillis() + val age = now - packet.timestamp.toLong() + if (age > com.bitchat.android.util.AppConstants.STALE_PEER_TIMEOUT_MS) { + Log.w(TAG, "Ignoring stale ANNOUNCE from ${peerID.take(8)} (age=${age}ms > ${com.bitchat.android.util.AppConstants.STALE_PEER_TIMEOUT_MS}ms)") + return false + } // Try to decode as iOS-compatible IdentityAnnouncement with TLV format val announcement = IdentityAnnouncement.decode(packet.payload) diff --git a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt index 68a539eb..d61beee0 100644 --- a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt @@ -67,9 +67,11 @@ class PeerManager { companion object { private const val TAG = "PeerManager" - private const val STALE_PEER_TIMEOUT = 180000L // 3 minutes (same as iOS) private const val CLEANUP_INTERVAL = 60000L // 1 minute } + + // Centralized timeout from AppConstants + private val stalePeerTimeoutMs: Long = com.bitchat.android.util.AppConstants.STALE_PEER_TIMEOUT_MS // Peer tracking data - enhanced with verification status private val peers = ConcurrentHashMap() // peerID -> PeerInfo @@ -299,7 +301,7 @@ class PeerManager { fun isPeerActive(peerID: String): Boolean { val info = peers[peerID] ?: return false val now = System.currentTimeMillis() - return (now - info.lastSeen) <= STALE_PEER_TIMEOUT && info.isConnected + return (now - info.lastSeen) <= stalePeerTimeoutMs && info.isConnected } /** @@ -328,7 +330,7 @@ class PeerManager { */ fun getActivePeerIDs(): List { val now = System.currentTimeMillis() - return peers.filterValues { (now - it.lastSeen) <= STALE_PEER_TIMEOUT && it.isConnected } + return peers.filterValues { (now - it.lastSeen) <= stalePeerTimeoutMs && it.isConnected } .keys .toList() .sorted() @@ -426,7 +428,7 @@ class PeerManager { private fun cleanupStalePeers() { val now = System.currentTimeMillis() - val peersToRemove = peers.filterValues { (now - it.lastSeen) > STALE_PEER_TIMEOUT } + val peersToRemove = peers.filterValues { (now - it.lastSeen) > stalePeerTimeoutMs } .keys .toList() diff --git a/app/src/main/java/com/bitchat/android/sync/GossipSyncManager.kt b/app/src/main/java/com/bitchat/android/sync/GossipSyncManager.kt index 38d23a91..05601bfc 100644 --- a/app/src/main/java/com/bitchat/android/sync/GossipSyncManager.kt +++ b/app/src/main/java/com/bitchat/android/sync/GossipSyncManager.kt @@ -31,7 +31,10 @@ class GossipSyncManager( fun gcsTargetFpr(): Double // percent -> 0.0..1.0 } - companion object { private const val TAG = "GossipSyncManager" } + companion object { + private const val TAG = "GossipSyncManager" + private const val CLEANUP_INTERVAL = 60_000L // 1 minute + } var delegate: Delegate? = null @@ -46,6 +49,7 @@ class GossipSyncManager( private val latestAnnouncementByPeer = ConcurrentHashMap>() private var periodicJob: Job? = null + private var cleanupJob: Job? = null fun start() { periodicJob?.cancel() periodicJob = scope.launch(Dispatchers.IO) { @@ -57,10 +61,23 @@ class GossipSyncManager( catch (e: Exception) { Log.e(TAG, "Periodic sync error: ${e.message}") } } } + + // Start periodic cleanup of stale announcements and messages + cleanupJob?.cancel() + cleanupJob = scope.launch(Dispatchers.IO) { + while (isActive) { + try { + delay(CLEANUP_INTERVAL) + pruneStaleAnnouncements() + } catch (e: CancellationException) { throw e } + catch (e: Exception) { Log.e(TAG, "Periodic cleanup error: ${e.message}") } + } + } } fun stop() { periodicJob?.cancel(); periodicJob = null + cleanupJob?.cancel(); cleanupJob = null } fun scheduleInitialSync(delayMs: Long = 5_000L) { @@ -98,6 +115,13 @@ class GossipSyncManager( } } } else if (isAnnouncement) { + // Ignore stale announcements older than STALE_PEER_TIMEOUT + val now = System.currentTimeMillis() + val age = now - packet.timestamp.toLong() + if (age > com.bitchat.android.util.AppConstants.STALE_PEER_TIMEOUT_MS) { + Log.d(TAG, "Ignoring stale ANNOUNCE (age=${age}ms > ${com.bitchat.android.util.AppConstants.STALE_PEER_TIMEOUT_MS}ms)") + return + } // senderID is fixed-size 8 bytes; map to hex string for key val sender = packet.senderID.joinToString("") { b -> "%02x".format(b) } latestAnnouncementByPeer[sender] = id to packet @@ -235,6 +259,42 @@ class GossipSyncManager( return RequestSyncPacket(p = params.p, m = mVal, data = params.data).encode() } + // Periodically remove stale announcements and all their messages + private fun pruneStaleAnnouncements() { + val now = System.currentTimeMillis() + val stalePeers = mutableListOf() + + // Identify stale announcements by age + for ((peerID, pair) in latestAnnouncementByPeer.entries) { + val pkt = pair.second + val age = now - pkt.timestamp.toLong() + if (age > com.bitchat.android.util.AppConstants.STALE_PEER_TIMEOUT_MS) { + stalePeers.add(peerID) + } + } + + if (stalePeers.isEmpty()) return + + // Remove announcements and their messages + var totalPrunedMsgs = 0 + for (peerID in stalePeers) { + // Count messages to be pruned for logging + val toRemove = mutableListOf() + synchronized(messages) { + for ((id, message) in messages) { + val sender = message.senderID.joinToString("") { b -> "%02x".format(b) } + if (sender == peerID) toRemove.add(id) + } + } + totalPrunedMsgs += toRemove.size + + // Reuse existing removal which also clears announcement entry + removeAnnouncementForPeer(peerID) + } + + Log.d(TAG, "Pruned ${stalePeers.size} stale announcements and $totalPrunedMsgs messages") + } + // Explicitly remove stored announcement for a given peer (hex ID) fun removeAnnouncementForPeer(peerID: String) { val key = peerID.lowercase() @@ -244,16 +304,20 @@ class GossipSyncManager( // Collect IDs to remove first to avoid modifying collection while iterating val idsToRemove = mutableListOf() - for ((id, message) in messages) { - val sender = message.senderID.joinToString("") { b -> "%02x".format(b) } - if (sender == key) { - idsToRemove.add(id) + synchronized(messages) { + for ((id, message) in messages) { + val sender = message.senderID.joinToString("") { b -> "%02x".format(b) } + if (sender == key) { + idsToRemove.add(id) + } } } // Now remove the collected IDs - for (id in idsToRemove) { - messages.remove(id) + synchronized(messages) { + for (id in idsToRemove) { + messages.remove(id) + } } if (idsToRemove.isNotEmpty()) { diff --git a/app/src/main/java/com/bitchat/android/util/AppConstants.kt b/app/src/main/java/com/bitchat/android/util/AppConstants.kt new file mode 100644 index 00000000..18c8e269 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/util/AppConstants.kt @@ -0,0 +1,10 @@ +package com.bitchat.android.util + +/** + * Centralized application-wide constants. + */ +object AppConstants { + // Peer staleness threshold (ms). Keep consistent across mesh and sync layers. + const val STALE_PEER_TIMEOUT_MS: Long = 180_000L // 3 minutes +} +