How BitChat's Gossip Sync Protocol Maintains Message History Across Device Restarts and Network Partitions
BitChat uses a gossip-based sync manager (GossipSyncManager) that combines bounded in-memory packet stores, freshness-based filtering, and persistent JSON archives to maintain a rolling window of recent public messages that survives restarts and enables back-filling for offline peers.
The gossip sync protocol in BitChat (implemented in the permissionlesstech/bitchat repository) solves a critical problem for decentralized mesh messaging: how do devices recover messages they missed while offline, without requiring always-on servers? The answer lies in a carefully engineered hybrid of memory-efficient caching, age-based expiration, and disk persistence that keeps recent public chat history available for re-gossiping to rejoining peers.
Core Architecture: Packet Stores and Bounded Caches
At the heart of the protocol is GossipSyncManager, defined in bitchat/Sync/GossipSyncManager.swift. The manager maintains a dedicated PacketStore for each sync type—including messages, fragments, file transfers, group messages, announcements, and pre-key bundles. Each store enforces a strict capacity limit and maintains packet IDs in insertion order, ensuring FIFO eviction when thresholds are exceeded.
// From GossipSyncManager.swift - packet store initialization
private let messageStore: PacketStore<PublicMessagePacket>
private let fragmentStore: PacketStore<FileTransferFragmentPacket>
// ... additional stores for each sync type
The store's capacity parameter (configurable per GossipSyncManager.Config) determines how many packets remain in memory. This design prevents unbounded memory growth while preserving enough history for effective gossip synchronization.
Freshness Filtering: Age-Based Message Validation
Every incoming packet passes through _onPublicPacketSeen, which enforces temporal boundaries before storage. The manager consults three configuration parameters:
maxMessageAgeSeconds— general message lifetimepublicMessageMaxAgeSeconds— public chat specific windowprekeyBundleMaxAgeSeconds— cryptographic key material freshness
Only packets falling within these windows are admitted to stores. This freshness filtering occurs in GossipSyncManager.swift lines 45-61, ensuring stale traffic never pollutes caches or propagates to peers.
func onPublicPacketSeen(_ packet: GossipPacket) {
// Age validation happens here before store insertion
guard packet.isWithin(freshnessWindow) else { return }
messageStore.insert(packet)
dirtyFlags.insert(.messages)
}
Persistent Archive: Surviving Application Restarts
Memory caches alone would lose data on termination. BitChat solves this through GossipMessageArchive, which maintains a JSON-backed copy of public messages in the device's Application Support directory.
When GossipSyncManager initializes with an archive instance (lines 123-138), it asynchronously executes restoreArchivedMessages():
// Archive restoration on startup
init(archive: GossipMessageArchive?, ...) {
self.archive = archive
restoreArchivedMessages() // Loads fresh messages from disk
}
The archive uses lightweight JSON serialization, with dirty-flag tracking to minimize write amplification—only modified data flushes to disk.
Periodic Maintenance: Automated Cleanup and Persistence
A DispatchSourceTimer drives background housekeeping every maintenanceIntervalSeconds (default: 30 seconds). This timer coordinates three critical tasks defined in lines 68-77 and 98-105:
cleanupExpiredMessages()— removes entries exceeding age thresholds- Stale announcement pruning — purges outdated network presence data
persistArchiveIfDirty()— commits memory state to disk when flagged
private func startMaintenanceTimer() {
timer.schedule(deadline: .now(), repeating: .seconds(config.maintenanceIntervalSeconds))
timer.setEventHandler { [weak self] in
self?.cleanupExpiredMessages()
self?.persistArchiveIfDirty()
}
}
Sync Rounds: Golomb-Coded Set Exchange for Gap Recovery
The protocol's gossip synchronization mechanism uses periodic GCS (Golomb-Coded Set) exchanges to detect missing packets. Each sync type operates on its own schedule:
messageSyncIntervalSecondsfragmentSyncIntervalSecondsgroupMessageSyncIntervalSeconds
During a sync round, buildGcsPayload() constructs a compact probabilistic filter containing IDs of fresh packets. The manager transmits a REQUEST_SYNC packet to connected peers, who respond with any IDs absent from the filter—enabling targeted back-filling without full history transfer.
This mechanism, spanning lines 24-33 and 124-138, ensures efficient bandwidth usage even as mesh topology shifts.
Pre-Key Bundle Security: Anti-Flooding Protection
Cryptographic key material receives special handling. Rather than a standard PacketStore, pre-key bundles reside in latestPrekeyBundleByPeer, a dictionary keyed by noise static public key. This structure:
- Retains only one bundle per peer owner
- Prevents attackers from exhausting cache capacity with forged bundles
- Integrates with the same freshness and persistence pipelines
The implementation appears in lines 86-106, demonstrating how security considerations influence storage architecture.
Practical Integration: Using the Gossip Sync Manager
The following pattern illustrates typical manager lifecycle management within the BitChat Bluetooth transport layer:
// 1️⃣ Create a manager with persistence
let archive = GossipMessageArchive() // ~/Library/Application Support/sync/public-messages.json
let requestSyncMgr = RequestSyncManager()
let gossipMgr = GossipSyncManager(
myPeerID: myPeerID,
config: GossipSyncManager.Config(),
requestSyncManager: requestSyncMgr,
archive: archive
)
// 2️⃣ Start periodic maintenance
gossipMgr.start()
// 3️⃣ Feed a newly-received public message (e.g. from BLE)
gossipMgr.onPublicPacketSeen(incomingPacket)
// 4️⃣ When the app goes to background, force a flush
gossipMgr.persistNow()
// 5️⃣ Retrieve fresh public messages for UI "timeline echo"
gossipMgr.collectPublicMessagePackets { packets in
// packets contains only messages within publicMessageMaxAgeSeconds
display(packets)
}
Key Source Files
| File | Purpose |
|---|---|
bitchat/Sync/GossipSyncManager.swift |
Core sync engine: packet stores, freshness checks, periodic sync, request handling, archive persistence |
bitchat/Sync/GossipMessageArchive.swift |
Disk-level JSON archive for restart survival |
docs/REQUEST_SYNC_MANAGER.md |
Protocol design documentation for GCS-based synchronization |
bitchat/Services/BLE/BLEService.swift |
Integration example showing manager instantiation in Bluetooth transport |
Summary
BitChat's gossip sync protocol maintains message history through five coordinated mechanisms:
- Bounded in-memory stores with FIFO eviction prevent unbounded growth
- Freshness filtering enforces temporal windows on all stored packets
- JSON archive persistence enables recovery across application restarts
- Periodic maintenance timers automate cleanup and disk synchronization
- GCS-based sync rounds efficiently detect and fill gaps for offline peers
This architecture—implemented primarily in GossipSyncManager.swift—delivers serverless mesh messaging with reliable history preservation despite device churn and network partitions.
Frequently Asked Questions
How does BitChat prevent old messages from filling up device storage?
BitChat employs dual-layer expiration: in-memory stores enforce capacity limits with FIFO eviction, while freshness filters reject packets exceeding age thresholds (maxMessageAgeSeconds, publicMessageMaxAgeSeconds). The periodic maintenance timer (default 30-second interval) actively purges expired entries from both memory and archive.
What happens to message history when a device restarts?
The GossipMessageArchive persists fresh public messages to JSON in the Application Support directory. On startup, GossipSyncManager.restoreArchivedMessages() reloads any still-fresh messages into memory, restoring gossip capability without network dependency. Stale messages are discarded during restoration.
How does BitChat synchronize missed messages without central servers?
Peers exchange Golomb-Coded Sets (GCS) containing packet IDs they currently hold. A receiving peer compares this filter against its own store and responds with missing packets. This REQUEST_SYNC protocol operates independently per packet type on configurable intervals, enabling targeted back-filling without full history broadcasts.
Why are pre-key bundles stored separately from regular messages?
Pre-key bundles use a dedicated dictionary (latestPrekeyBundleByPeer) keyed by cryptographic identity to enforce single-bundle-per-peer limits. This prevents denial-of-service attacks that could otherwise exhaust store capacity with forged bundles, while still benefiting from the same freshness validation and persistence infrastructure.
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 →