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:

  1. Type validation — Verifies the MessageType and confirms recipientID == 0xFFFFFFFF… (public address)
  2. Freshness check — Calls isPacketFresh with type-specific windows (15 minutes for messages, 24 hours for pre-key bundles)
  3. Bounded insertion — Adds to a per-type PacketStore with 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: _onPublicPacketSeen in [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:

  1. Prunes expired data via cleanupExpiredMessages
  2. Removes stale announcements via cleanupStaleAnnouncementsIfNeeded
  3. Triggers per-type sync through sendPeriodicSync for each configured SyncSchedule

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: performPeriodicMaintenance and sendPeriodicSync in GossipSyncManager.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 within gcsMaxBytes
  • Cursor calculation — Computes sinceTimestamp marking 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: buildGcsPayload in GossipSyncManager.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:

  1. Rate limiting — SyncResponseRateLimiter prevents response flooding
  2. Filter decoding — GCSFilter.decodeToSortedSet reconstructs the requester's known IDs
  3. Selective iteration — For each type, skips packets older than sinceTimestamp and IDs present in the filter (mightContain)
  4. RSR marking — Sets ttl = 0 and isRSR = true on each response packet
  5. 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: _handleRequestSync in GossipSyncManager.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: isValidResponse in [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: persistArchiveIfDirty and removeAllPublicMessages in GossipSyncManager.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: init in GossipSyncManager.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: scheduleInitialSyncToPeer in GossipSyncManager.swift

Recovering Stalled Fragment Streams

// When fragment reassembly detects missing chunks
let stalledIDs: [Data] = reassemblyContext.stalledStreamIDs
gossipMgr.requestMissingFragments(fragmentIDs: stalledIDs)

Source: requestMissingFragments in GossipSyncManager.swift

Background Persistence

// In AppDelegate or SceneDelegate
func applicationDidEnterBackground() {
    gossipMgr.persistNow()
}

Source: persistNow in GossipSyncManager.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 — PacketStore with per-type capacity limits prevents unbounded memory growth
  • Bandwidth-efficient filtering — GCS filters encode known packet IDs compactly, minimizing redundant transfers
  • Security through attribution — RequestSyncManager links responses to prior requests, blocking spoofed replays
  • Automatic persistence — GossipMessageArchive maintains 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →