How BitChat's Gossip Sync Protocol Synchronizes Message History Across Mesh Networks
BitChat's gossip sync protocol uses bounded packet stores, Golomb-Coded Set filters, and request-response attribution to efficiently back-fill public message history while preventing replay attacks.
The gossip sync protocol in permissionlesstech/bitchat enables decentralized peers to discover and exchange missing public messages without centralized servers. Built for Bluetooth Low Energy mesh environments, the protocol balances bandwidth efficiency with security through compact probabilistic filters and strict request validation.
Core Architecture: Three Key Components
The gossip sync protocol centers on three cooperating systems:
| Component | Responsibility |
|---|---|
| GossipSyncManager | Maintains bounded, time-limited stores of recent packets and drives periodic sync rounds |
| RequestSyncManager | Registers outgoing requests and validates incoming responses to prevent spoofing |
| GCS Filter | Encodes packet IDs the requester already holds, letting responders skip known data |
These components work together in bitchat/Sync/GossipSyncManager.swift and bitchat/Sync/RequestSyncManager.swift to provide reliable history synchronization across intermittent mesh connections.
Storing Recent Packets
When any peer receives a broadcast packet, GossipSyncManager processes it through _onPublicPacketSeen:
- Type validation — Verifies the
MessageTypeand confirmsrecipientID == 0xFFFFFFFF…(public address) - Freshness check — Calls
isPacketFreshwith type-specific windows (15 minutes for messages, 24 hours for pre-key bundles) - Bounded insertion — Adds to a per-type
PacketStorewith capacity limits (seenCapacity,fragmentCapacity), evicting oldest entries when full
The manager also persists public messages via GossipMessageArchive, ensuring the gossip store survives app restarts.
// Packet stores are configured per type with specific capacities
struct GossipSyncConfig {
let seenCapacity: Int // For standard messages
let fragmentCapacity: Int // For file fragments
let prekeyCapacity: Int // For X3DH pre-keys
let maintenanceIntervalSeconds: TimeInterval = 30.0
}
Source:
_onPublicPacketSeenin [GossipSyncManager.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/GossipSyncManager.swift#L52-L86)
Periodic Sync Rounds
A periodicTimer fires every maintenanceIntervalSeconds (default 30 seconds). Each tick executes performPeriodicMaintenance:
- Prunes expired data via
cleanupExpiredMessages - Removes stale announcements via
cleanupStaleAnnouncementsIfNeeded - Triggers per-type sync through
sendPeriodicSyncfor each configuredSyncSchedule
sendPeriodicSync prefers unicast REQUEST_SYNC packets to every connected peer, enabling proper RSR attribution. It falls back to broadcast only when no peers are connected.
Source:
performPeriodicMaintenanceandsendPeriodicSyncinGossipSyncManager.swift
Building Compact Sync Requests
The buildGcsPayload(for:types:) method constructs the sync request payload through three steps:
- Candidate gathering — Collects packets of requested types, sorted newest-first
- Filter construction — Derives GCS parameters (
p,m) fitting withingcsMaxBytes - Cursor calculation — Computes
sinceTimestampmarking the oldest filtered packet
The resulting RequestSyncPacket contains:
| Field | Purpose |
|---|---|
p, m, filter data |
GCS filter encoding packet IDs the requester holds |
types |
Bit-field of requested packet categories |
sinceTimestamp (optional) |
Oldest timestamp covered by filter |
fragmentIdFilter (optional) |
Specific fragment stream IDs for targeted re-assembly |
Source:
buildGcsPayloadinGossipSyncManager.swift
Request Registration and Security
Before transmission, GossipSyncManager registers each request:
requestSyncManager.registerRequest(to: peerID)
RequestSyncManager stores pendingRequests[peerID] = now(), creating a 30-second validation window. This registration enables late rejection of spoofed responses.
Source:
registerRequest(to:)in [RequestSyncManager.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/RequestSyncManager.swift#L32-L40)
Responding to Sync Requests
When a peer receives REQUEST_SYNC, _handleRequestSync executes:
- Rate limiting —
SyncResponseRateLimiterprevents response flooding - Filter decoding —
GCSFilter.decodeToSortedSetreconstructs the requester's known IDs - Selective iteration — For each type, skips packets older than
sinceTimestampand IDs present in the filter (mightContain) - RSR marking — Sets
ttl = 0andisRSR = trueon each response packet - Direct transmission — Sends via
delegate?.sendPacket(to: peerID, packet: toSend)
Only packets missing from the filter are transmitted, dramatically reducing bandwidth versus naive flooding.
Source:
_handleRequestSyncinGossipSyncManager.swift
Validating Incoming Responses
The BLE security layer performs strict RSR validation:
// In BLEService packet processing
if packet.isRSR {
guard requestSyncManager.isValidResponse(from: packet.senderID) else {
return .reject // Unsolicited or expired
}
// Valid RSR: accept even with old timestamps
}
RequestSyncManager.isValidResponse confirms:
- A pending request exists for the sender
- The packet arrives within
responseWindow(30 seconds)
This validation allows historic packets (beyond the normal 2-minute clock window) while blocking replay attacks.
Source:
isValidResponsein [RequestSyncManager.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/RequestSyncManager.swift#L42-L66)
Persistence and Cleanup
Post-maintenance, persistArchiveIfDirty writes modified public messages to disk when archiveDirty is set. The archive also flushes on app backgrounding via persistNow(). Users can clear all public history through removeAllPublicMessages, which empties both memory and disk stores.
Sources:
persistArchiveIfDirtyandremoveAllPublicMessagesinGossipSyncManager.swift
Practical Implementation Examples
Initializing the Gossip Sync Engine
// Create supporting managers
let requestSync = RequestSyncManager()
let gossipArchive = GossipMessageArchive()
// Initialize GossipSyncManager
let gossipMgr = GossipSyncManager(
myPeerID: myPeerID,
requestSyncManager: requestSync,
archive: gossipArchive
)
// Integrate with BLE transport and start
bleService.gossipSyncManager = gossipMgr
gossipMgr.start()
Source:
initinGossipSyncManager.swift
Scheduling Immediate Sync with New Peers
// BLEService calls this when connection handshake completes
func didConnect(to peerID: PeerID) {
// Wait 5 seconds for initial routing table exchange
gossipMgr.scheduleInitialSyncToPeer(peerID, delaySeconds: 5.0)
}
Source:
scheduleInitialSyncToPeerinGossipSyncManager.swift
Recovering Stalled Fragment Streams
// When fragment reassembly detects missing chunks
let stalledIDs: [Data] = reassemblyContext.stalledStreamIDs
gossipMgr.requestMissingFragments(fragmentIDs: stalledIDs)
Source:
requestMissingFragmentsinGossipSyncManager.swift
Background Persistence
// In AppDelegate or SceneDelegate
func applicationDidEnterBackground() {
gossipMgr.persistNow()
}
Source:
persistNowinGossipSyncManager.swift
Key Source Files
| File | Function |
|---|---|
bitchat/Sync/GossipSyncManager.swift |
Core gossip sync: stores, timers, request building, response handling |
bitchat/Sync/RequestSyncManager.swift |
Request registration and RSR validation |
bitchat/Sync/GossipMessageArchive.swift |
Disk persistence for public messages |
bitchat/Sync/GCSFilter.swift |
Golomb-Coded Set implementation |
bitchat/Services/BLE/BLEService.swift |
Transport integration and security enforcement |
docs/REQUEST_SYNC_MANAGER.md |
Protocol design documentation |
Summary
- Bounded storage —
PacketStorewith per-type capacity limits prevents unbounded memory growth - Bandwidth-efficient filtering — GCS filters encode known packet IDs compactly, minimizing redundant transfers
- Security through attribution —
RequestSyncManagerlinks responses to prior requests, blocking spoofed replays - Automatic persistence —
GossipMessageArchivemaintains history across app restarts - Periodic and on-demand sync — 30-second maintenance rounds plus targeted fragment recovery
Frequently Asked Questions
How does BitChat's gossip sync protocol prevent bandwidth waste?
The protocol uses Golomb-Coded Set (GCS) filters to compactly represent packet IDs the requester already possesses. Responders iterate their local store and skip any ID where mightContain returns true, transmitting only missing packets. This reduces sync traffic from O(n) to approximately the size of the set difference.
What stops malicious peers from flooding old messages as fake sync responses?
RequestSyncManager requires request-response attribution. Every REQUEST_SYNC registers the target peer and timestamp; incoming packets with isRSR = true are rejected unless a matching pending request exists within the 30-second responseWindow. Unsolicited historic packets fail validation and are dropped.
Why does the protocol use unicast requests instead of broadcast?
Unicast requests enable proper RSR attribution—each response can be validated against a specific peer registration. Broadcast requests are used only as fallback when no peers are connected, sacrificing attribution guarantees for basic functionality.
How does the gossip store handle app restarts?
GossipMessageArchive persists public packets to disk. The archive loads on GossipSyncManager initialization, repopulating the in-memory PacketStores. Dirty-tracking via archiveDirty ensures writes occur only when necessary, with forced flushing on backgrounding through persistNow().
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →