# How BitChat's Gossip Sync Protocol Synchronizes Message History Across Mesh Networks

> Discover how BitChat's gossip sync protocol efficiently back-fills message history across mesh networks using bounded packet stores and Golomb-Coded Set filters, while preventing replay attacks.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-08-07

---

**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](https://github.com/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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/GossipSyncManager.swift) and [`bitchat/Sync/RequestSyncManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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.

```swift
// 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/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/GossipSyncManager.swift)

## Request Registration and Security

Before transmission, `GossipSyncManager` registers each request:

```swift
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/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`](https://github.com/permissionlesstech/bitchat/blob/main/GossipSyncManager.swift)

## Validating Incoming Responses

The BLE security layer performs strict RSR validation:

```swift
// 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/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`](https://github.com/permissionlesstech/bitchat/blob/main/GossipSyncManager.swift)

## Practical Implementation Examples

### Initializing the Gossip Sync Engine

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/GossipSyncManager.swift)

### Scheduling Immediate Sync with New Peers

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/GossipSyncManager.swift)

### Recovering Stalled Fragment Streams

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

```

> **Source:** `requestMissingFragments` in [`GossipSyncManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/GossipSyncManager.swift)

### Background Persistence

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

```

> **Source:** `persistNow` in [`GossipSyncManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/GossipSyncManager.swift)

## Key Source Files

| File | Function |
|------|----------|
| [`bitchat/Sync/GossipSyncManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/GossipSyncManager.swift) | Core gossip sync: stores, timers, request building, response handling |
| [`bitchat/Sync/RequestSyncManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/RequestSyncManager.swift) | Request registration and RSR validation |
| [`bitchat/Sync/GossipMessageArchive.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/GossipMessageArchive.swift) | Disk persistence for public messages |
| [`bitchat/Sync/GCSFilter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/GCSFilter.swift) | Golomb-Coded Set implementation |
| [`bitchat/Services/BLE/BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift) | Transport integration and security enforcement |
| [`docs/REQUEST_SYNC_MANAGER.md`](https://github.com/permissionlesstech/bitchat/blob/main/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 `PacketStore`s. Dirty-tracking via `archiveDirty` ensures writes occur only when necessary, with forced flushing on backgrounding through `persistNow()`.