How BitChat's P2P Network Layers Are Implemented: A Deep Dive Into the BLE Mesh Stack
BitChat implements its P2P network as a four-layer stack separating BLE radio handling, protocol state, feature modules, and transport abstractions, with deterministic simulation support and Nostr/Tor fallback bridges.
BitChat's peer-to-peer networking architecture follows a strictly layered design that enables testability, cross-platform portability, and graceful degradation from BLE mesh to internet bridges. This guide examines each layer's responsibilities, concurrency contracts, and implementation details based on the permissionlesstech/bitchat source code.
The Four-Layer P2P Network Stack
BitChat's P2P network layers are organized from physical radio up to application boundary:
| Layer | Responsibility | Core Abstraction |
|---|---|---|
| BLE Link Layer | Bluetooth Low-Energy radio operations | BLELinkLayer / SimulatedLinkLayer |
| Mesh Engine | Protocol state, encryption, routing | Sans-I/O core with handle(event) -> [Effect] |
| Feature Modules | Functional subsystems (chat, voice, groups) | Self-contained modules with private state |
| App Boundary / Transport | Public façade for UI and external systems | Transport protocol with capability discovery |
BLE Link Layer: Isolated Radio Handling
The BLE Link Layer in BLE-ARCHITECTURE-V3.md owns all direct CoreBluetooth interaction. This is the only layer that imports CoreBluetooth, ensuring clean separation from higher-level logic.
Key Responsibilities
- Advertising and scanning scheduling
- Connection management and MTU negotiation
- Back-pressure buffering for link writes
- Emitting
LinkEvent(link up/down, bytes received, writable state) - Consuming
LinkCommand(send bytes, start/stop scanning)
The layer runs on a dedicated bleQueue serial queue. All link-state mutations are confined to this queue, preventing race conditions with the protocol engine.
Simulated Link Layer for Testing
For deterministic testing without hardware, SimulatedLinkLayer implements the same interface. This enables multi-node property-based tests in SimulatedMeshTests.swift that verify protocol behavior across simulated topologies.
// The link layer emits events consumed by the engine
protocol LinkLayer {
var events: AsyncStream<LinkEvent> { get }
func execute(_ command: LinkCommand) async
}
// Real implementation uses CoreBluetooth
final class BLELinkLayer: LinkLayer { /* ... */ }
// Simulated implementation for testing
final class SimulatedLinkLayer: LinkLayer { /* ... */ }
Mesh Engine: Sans-I/O Protocol Core
The Mesh Engine contains BitChat's core protocol implementation in BitFoundation. It is intentionally sans-I/O—pure logic that transforms events into effects, making it hardware-agnostic and trivially testable.
Architecture Principles
All mutable state lives in the engine, accessed exclusively through a single-writer engine serial queue. Cross-thread access uses onEngine scheduling primitives. This design appears in:
BitchatPacket.swift— packet encapsulation and fragmentationNoiseSession.swiftandNoiseSessionManager.swift— Noise XX handshake and encryptionBitchatMessage.swift— message model with mesh channel routingPeerID.swift— peer identifiers with mesh prefix (mesh:)
Effect-Based Core Loop
The engine's central function has this signature:
func handle(event: EngineEvent) -> [Effect]
Effects describe actual operations—send packet, update peer registry, notify feature modules—while the core remains pure and synchronous.
Post-Migration Link-Auth Binding
After architectural migration, the engine owns BLELinkAuthState and BLELinkBindings. This guarantees atomic "check-binding + act" semantics when mapping physical links to peer identities.
Feature Modules: Extensible Functional Blocks
Feature Modules implement independent subsystems without modifying engine core. Each module:
- Owns its private state store
- Registers for specific message types via
MessageType - Lives in sources like
CourierEnvelope.swift, voice handling inMessageType.swift, orMeshDiagnostics.swift
Adding functionality requires creating a new module, not touching protocol internals. This extends to courier routing, private media, groups, verification, and diagnostics.
App Boundary and Transport Abstraction
The Transport layer provides the public façade used by UI components. Defined in Transport.swift, it exposes a minimal protocol that concrete implementations satisfy:
// Core transport used throughout the UI
protocol Transport {
func send(_ message: BitchatMessage) async throws
var isMeshReachable: Bool { get }
// ...
}
Capability protocols discovered via as? casting extend this:
MeshBridgingTransport— Nostr bridge fallbackPanicResettingTransport— hard reset capabilityBluetoothStateReporting— radio state inspection
The AppRuntime and feature models inject concrete transport implementations based on runtime conditions.
Concurrency Contracts Across Layers
BitChat's P2P network layers enforce three ownership domains to prevent deadlocks:
engine— engine queue, single-writer for protocol statebleQueue— BLE operations, isolated from engine- Lock-backed stores — e.g.,
BLEPeerRegistryStorefor peer metadata
No domain synchronously waits on another. Only asynchronous hand-offs are permitted. This contract is validated in BLEQueueContractTests.swift (see §55-71 of BLE-ARCHITECTURE-V3.md).
Fallback: Nostr and Tor Bridging
When BLE mesh cannot reach a peer, BitChat bridges traffic via Nostr—a decentralized pub-sub network—with optional Tor for censorship resistance.
The MeshBridgingTransport capability signs payloads with a per-cell Nostr identity and publishes to geohash-based rendezvous topics. Wire format details are documented in SOURCE_ROUTING.md and PEER-ID-ROTATION.md.
// Check mesh reachability before bridging
if !transport.isMeshReachable {
// Casts to bridging capability if available
if let bridge = transport as? MeshBridgingTransport {
await bridge.send(message) // Signs with Nostr key, publishes
}
}
Practical Usage Examples
Creating and sending mesh messages follows this pattern:
// 1. Create mesh-prefixed peer identifier
let meshPeer = PeerID(str: "mesh:\(UUID().uuidString)")
// PeerID.swift: https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/PeerID.swift
// 2. Build message for mesh channel
let msg = BitchatMessage(
id: .init(),
sender: myPeerID,
channelID: .mesh,
timestamp: Date(),
type: .chat,
payload: Data("Hello mesh!".utf8)
)
// BitchatMessage.swift: https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/BitchatMessage.swift
// 3. Send through transport façade (encrypts with Noise, fragments, enqueues)
await transport.send(msg)
// Transport.swift: https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Transport.swift
Key Source Files by Layer
| Layer | Primary Files |
|---|---|
| BLE Link Layer | BLE-ARCHITECTURE-V3.md, BLEService+LinkLayerCentralRole.swift, BLEService+LinkLayerPeripheralRole.swift |
| Mesh Engine | BitchatPacket.swift, NoiseSession.swift, NoiseSessionManager.swift, BitchatMessage.swift, PeerID.swift, PeerCapabilities.swift |
| Feature Modules | CourierEnvelope.swift, MessageType.swift (voice), MeshDiagnostics.swift |
| Transport / UI | Transport.swift, MeshBridgingTransport.swift, AppRuntime |
| Simulation / Tests | SimulatedMesh.swift, SimulatedMeshTests.swift, BLEQueueContractTests.swift |
Summary
- Four explicit layers separate BLE radio, protocol logic, features, and transport abstractions
- Sans-I/O engine core enables hardware-free testing via
SimulatedLinkLayer - Three-domain concurrency contract (
engine,bleQueue, lock stores) guarantees deadlock-free operation - Capability-based transport design allows runtime discovery of Nostr bridging, panic reset, and Bluetooth state reporting
- Nostr/Tor fallback extends reachability when BLE mesh is insufficient
Frequently Asked Questions
What is the sans-I/O pattern used in BitChat's mesh engine?
The sans-I/O design means the engine core contains no actual network or hardware operations. Instead, it pure functions transform EngineEvent inputs into Effect outputs describing what should happen. Real I/O occurs at layer boundaries. This allows the entire protocol state machine to run in deterministic tests with SimulatedMesh, validating complex multi-peer interactions without physical devices.
How does BitChat prevent deadlocks between Bluetooth and protocol threads?
BitChat enforces three strict ownership domains: the engine serial queue for protocol state, the bleQueue serial queue for CoreBluetooth operations, and lock-protected stores for registry data. No domain may synchronously block on another—only async hand-offs are permitted. This architecture is documented in §55-71 of BLE-ARCHITECTURE-V3.md and validated by BLEQueueContractTests.swift.
Can BitChat's P2P network operate without Bluetooth hardware?
Yes. The SimulatedLinkLayer implements the same LinkLayer protocol as the real BLE implementation, feeding fabricated events into the engine. This enables SimulatedMeshTests.swift to run multi-node mesh scenarios deterministically, verifying packet routing, deduplication, and relay policy without any CoreBluetooth dependency.
How does message routing work when a peer is outside BLE range?
The MeshBridgingTransport capability provides fallback transport over Nostr, a decentralized pub-sub network. When transport.isMeshReachable returns false, the UI can cast to MeshBridgingTransport and call send(), which signs payloads with a per-cell Nostr identity and publishes to geohash-based topics. Optional Tor tunneling adds censorship resistance for internet-bridged traffic.
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 →