How the Gossip Protocol in BitChat Constructs the Mesh Topology Graph

BitChat's gossip protocol builds a mesh topology by having peers periodically broadcast signed announce packets containing up to ten direct neighbor claims, which are sanitized and stored in a concurrent claims map, with edges confirmed only when both endpoints reciprocate the claim before being used for BFS routing or UI visualization.

The gossip protocol in BitChat powers the decentralized peer discovery and routing layer of the permissionlesstech/bitchat repository. Through periodic announce broadcasts, nodes exchange 8-byte peer IDs representing reachable direct neighbors, constructing a directed adjacency graph that the MeshTopologyTracker validates and prunes to ensure only fresh, bidirectional connections form the mesh backbone.

The Gossip-Sync Round: Broadcasting Topology Claims

BitChat uses a gossip-sync round where each peer periodically broadcasts an announce packet. According to the source code in bitchat/Protocols/Packets.swift, this packet contains a directNeighbors field encoding up to ten neighbor peer IDs, each represented as 8-byte values.

When a signed announce is received, the GossipSyncManager._onPublicPacketSeen method in bitchat/Sync/GossipSyncManager.swift validates the packet signature and freshness before storing it in the latestAnnouncementByPeer dictionary:

func _onPublicPacketSeen(_ packet: BitchatPacket) {
    // ...
    case .announce:
        guard isPacketFresh(packet), isAnnouncementFresh(packet) else { return }
        let sender = PeerID(hexData: packet.senderID)
        latestAnnouncementByPeer[sender] = packet
        // BLEAnnounceHandler extracts directNeighbors and calls tracker.updateNeighbors
}

Processing Neighbor Claims in MeshTopologyTracker

The BLEAnnounceHandler decodes the announcement payload and extracts the directNeighbors array. It then calls MeshTopologyTracker.updateNeighbors(for:neighbors:) to record the topology claim.

Sanitizing Peer IDs and Removing Self-Loops

Located in bitchat/Services/MeshTopologyTracker.swift, the updateNeighbors method sanitizes input by truncating or padding peer IDs to the required 8-byte hop size and filtering out self-referential claims:

func updateNeighbors(for sourceData: Data?, neighbors: [Data], at now: Date = Date()) {
    guard let source = sanitize(sourceData) else { return }
    // Remove self-loops and truncate/pad IDs to 8 bytes
    let validNeighbors = Set(neighbors.compactMap { sanitize($0) }).subtracting([source])
    queue.sync(flags: .barrier) {
        self.claims[source] = validNeighbors
        self.lastSeen[source] = now
    }
}

The method writes to a thread-safe claims dictionary mapping RoutingID to a Set<RoutingID> of purported neighbors.

Confirming Bidirectional Edges and Pruning Stale Data

BitChat treats the topology as a directed graph where an edge exists only upon confirmation. An edge between two peers is considered valid only when both endpoints have reported each other in their respective announcements, as implemented in bitchat/Services/MeshTopologyTracker.swift.

Staleness Detection

The topology tracker enforces freshness through config.stalePeerTimeoutSeconds. The MeshTopologyTracker.prune method and GossipSyncManager.cleanupStaleAnnouncements remove nodes and claims that exceed the configured timeout, ensuring the mesh reflects only recently reachable peers.

Adjacency Snapshot for Routing

When computing routes or rendering the UI, the system queries adjacencySnapshot() to retrieve the current confirmed graph. The computeRoute method performs a BFS traversal over fresh, confirmed edges:

func computeRoute(from start: Data?, to goal: Data?, ...) -> [Data]? {
    // ...
    guard let neighbors = claims[last] else { continue }
    guard let lastSeenTime = lastSeen[last], lastSeenTime > freshnessDeadline else { continue }
    // Confirmed edge: both peers must claim each other
    guard let neighborClaims = claims[neighbor],
          neighborClaims.contains(last) else { continue }
    // ...
}

This implementation optionally enforces minimum protocol version requirements for every hop.

Visualizing the Mesh Topology

The UI layer consumes the same adjacency data through MeshTopologyView in bitchat/Views/MeshTopologyView.swift. The view receives a provider closure that constructs a MeshTopologyDisplayModel by calling tracker.adjacencySnapshot().

The snapshot translates claims into Node objects and edge tuples, which the view renders in a circular layout with the local peer positioned centrally. This provides a real-time visualization of the gossip-constructed mesh.

Summary

  • BitChat's gossip protocol broadcasts signed announce packets containing up to ten 8-byte direct neighbor IDs
  • GossipSyncManager._onPublicPacketSeen stores valid announces in latestAnnouncementByPeer
  • MeshTopologyTracker.updateNeighbors sanitizes IDs to 8-byte hop size and removes self-loops before storing in the thread-safe claims dictionary
  • Edges require bidirectional confirmation—both peers must claim each other—to be considered valid for routing
  • prune and cleanupStaleAnnouncements remove nodes exceeding stalePeerTimeoutSeconds to maintain graph freshness
  • computeRoute executes BFS over confirmed, fresh edges while MeshTopologyView visualizes the snapshot via adjacencySnapshot()

Frequently Asked Questions

How does BitChat prevent stale nodes from polluting the topology graph?

BitChat implements a timeout-based pruning mechanism. Nodes that fail to refresh their announce within config.stalePeerTimeoutSeconds are automatically removed by MeshTopologyTracker.prune and GossipSyncManager.cleanupStaleAnnouncements. This ensures that only recently active peers participate in the mesh topology, preventing routing attempts through disconnected nodes.

Why does BitChat require bidirectional claims to establish a mesh edge?

The protocol stores directed claims in the claims dictionary but only treats an edge as valid when both endpoints list each other. This bidirectional confirmation prevents unilateral spoofing and ensures that routing paths traverse actually reachable peers, as verified by reciprocal gossip announcements in MeshTopologyTracker.computeRoute.

What limits the number of neighbors announced in each gossip packet?

The announce packet structure defined in bitchat/Protocols/Packets.swift limits the directNeighbors array to ten entries. This constraint bounds packet size for BLE transmission while providing sufficient connectivity data to build an effective mesh topology through the gossip-sync round.

How does the gossip protocol handle malicious or spoofed topology claims?

While the announce packets are signed, the topology tracker primarily relies on freshness checks and bidirectional edge confirmation as tamper-resistance mechanisms. A malicious node cannot force a connection to an unwilling peer because edges require both parties to claim each other, and stale claims are pruned. Additionally, MeshTopologyTracker.updateNeighbors sanitizes all peer IDs to prevent malformed data from corrupting the graph.

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 →