How Bitchat Enables Deterministic Multi-Node Testing Without Physical Hardware

Bitchat's pure-Swift simulator replaces CoreBluetooth with an in-memory mesh of sans-I/O engines, allowing tests to advance virtual time and deliver packets deterministically without physical devices.

The permissionlesstech/bitchat repository includes a sophisticated testing harness that eliminates hardware dependencies for multi-node Bluetooth Low Energy (BLE) scenarios. By replacing the radio layer with a deterministic simulation environment, developers can execute complex protocol tests in milliseconds rather than minutes. This approach enables deterministic multi-node testing without physical hardware by funneling all side-effects through controllable hooks and a manual clock.

The Sans-I/O Engine Architecture

At the heart of Bitchat’s testability lies a sans-I/O engine core that strictly separates protocol logic from radio implementation. According to docs/BLE-ARCHITECTURE-V3.md, the engine exposes a pure function signature handle(event) → [Effect] where all side-effects—packet emission, link events, and timer scheduling—are returned as values rather than executed immediately.

This architectural split means the BLEService engine half contains zero direct Bluetooth calls. Instead, it relies on well-defined hooks that the simulator can intercept. The architecture documentation explains that this design "makes the engine formally handle(event) -> [Effect]", allowing the simulator to substitute a mock radio that captures effects in memory rather than transmitting over the air.

SimulatedMesh: Wiring Nodes Edge-to-Edge

The SimulatedMesh class orchestrates multi-node topologies by connecting BLEService instances through an outbound packet tap. Found in bitchatTests/Simulation/SimulatedMesh.swift, this component implements a deterministic model where:

  • Packet capture occurs via _test_onOutboundPacket, buffering transmissions under a lock-protected queue
  • Manual delivery happens through BLEEngineManualScheduler, which releases packets to neighbor nodes in a controlled loop
  • Quiescence detection relies on the pump() method, which processes the pendingDeliveries array round-by-round until the mesh stabilizes

Because the mesh bypasses CoreBluetooth entirely, nodes behave as if communicating via real radios while remaining fully observable. The source code shows this implementation between lines 5-15 for the mesh structure and lines 45-80 for the delivery logic.

Controlling Time with BLEEngineManualScheduler

Deterministic testing requires explicit control over asynchronous timing. The BLEEngineManualScheduler class implements the BLEEngineScheduling protocol used by each BLEService, providing a virtual clock that tests manipulate directly.

As implemented in bitchatTests/Mocks/BLEEngineManualScheduler.swift, the scheduler maintains a lock-protected now value representing the current virtual time. The critical advance(by:) method (lines 27-48) allows tests to step time forward by a specific interval, causing timer-driven work—such as relay jitter and deferred flushes—to execute exactly when the test decides:

func advance(by interval: TimeInterval) {
    let (due, queue): ([DispatchWorkItem], DispatchQueue?) = lock.withLock {
        now += interval
        let due = pending.filter { $0.deadline <= now }
                               .sorted { $0.deadline < $1.deadline }
                               .map(\.work)
        pending.removeAll { $0.deadline <= now }
        return (due, engineQueue)
    }
    guard let queue = queue else { return }
    due.forEach { queue.async(execute: $0) }
    queue.sync {}                 // fence: all work finished
}

The method sorts pending work by deadline, executes all due items, and synchronizes on the engine queue to guarantee completion before assertions run.

Determinism Guarantees and Performance

The simulator provides three key determinism guarantees that physical hardware cannot match:

  1. Deterministic packet ordering – Outbound packets are captured in a single tap and delivered via the pendingDeliveries array, processed in explicit rounds without race conditions
  2. Deterministic timing – All temporal behavior is driven by advanceTime(by:) calls, removing wall-clock nondeterminism from the execution path
  3. Isolated protocol logic – The mesh focuses on protocol-level behavior including announcement, binding, deduplication, TTL management, and Noise handshakes without exercising physical link-selection or back-pressure algorithms

This isolation yields significant performance benefits. According to the architecture documentation, the simulator executes five multi-node scenarios in approximately 40 milliseconds, compared to the minutes required for hardware-in-the-loop testing.

Practical Implementation Example

Creating a deterministic three-node test scenario requires minimal boilerplate:

// 1️⃣ Create a mesh with three deterministic nodes
let mesh = SimulatedMesh()
let alice = mesh.addNode(nickname: "alice")
let bob   = mesh.addNode(nickname: "bob")
let carol = mesh.addNode(nickname: "carol")

// 2️⃣ Wire the nodes together (full mesh)
mesh.connect(0, 1); mesh.connect(1, 2); mesh.connect(0, 2)

// 3️⃣ Force an announce from every node and pump the mesh
mesh.announceAll()               // each engine emits an announce packet
mesh.pump()                      // deliver packets until the mesh is quiet

// 4️⃣ Advance simulated time to trigger relay jitter / retries
mesh.advanceTime(by: 1.0)        // releases any scheduled work

The pump() method drives the mesh to quiescence, while advanceTime(by:) triggers any scheduled timers without waiting for real time to elapse.

Summary

  • Sans-I/O architecture separates BLEService logic from Bluetooth hardware through a pure handle(event) → [Effect] interface defined in BLE-ARCHITECTURE-V3.md
  • SimulatedMesh wires multiple engine instances via _test_onOutboundPacket taps and lock-protected delivery queues in SimulatedMesh.swift
  • Manual scheduling via BLEEngineManualScheduler provides deterministic timing control through the advance(by:) method, eliminating wall-clock dependencies
  • Protocol isolation allows testing of complex behaviors—Noise handshakes, TTL propagation, deduplication—without physical radio unpredictability
  • Performance reaches approximately 40 milliseconds for comprehensive multi-node scenarios, enabling rapid iteration in CI pipelines

Frequently Asked Questions

How does Bitchat simulate Bluetooth connections without CoreBluetooth?

Bitchat replaces the CoreBluetooth radio layer with SimulatedMesh, which intercepts outbound packets via the _test_onOutboundPacket hook and delivers them to neighboring nodes through an in-memory buffer. Each node runs a full BLEService engine that believes it is communicating via Bluetooth, but the transport is actually a deterministic message bus controlled by the test harness.

What makes the Bitchat simulator deterministic compared to real hardware testing?

Real hardware introduces nondeterministic variables including RF interference, timing jitter, and OS scheduling delays. The Bitchat simulator removes these variables by capturing all effects in the handle(event) → [Effect] return values, processing packets through a lock-protected pendingDeliveries array, and advancing time explicitly via BLEEngineManualScheduler.advance(by:). This ensures identical execution across test runs.

How does the manual scheduler control timing in multi-node tests?

The BLEEngineManualScheduler maintains a virtual clock as a lock-protected now variable. When tests call advanceTime(by:), the scheduler filters pending work items by deadline, sorts them deterministically, and executes them synchronously on the engine queue. This allows tests to trigger retry logic, relay jitter, and connection timeouts at specific moments rather than waiting for wall-clock time.

Can the simulator handle complex protocol scenarios like Noise handshakes and TTL propagation?

Yes. The simulator exercises the full protocol stack including cryptographic handshakes, message deduplication, and TTL-based routing because it runs the actual BLEService engine code. The architecture documentation notes that the mesh focuses on "protocol-level behavior (announcement, binding, deduplication, TTL, Noise handshakes)" while excluding only the physical link-selection layer, ensuring logical correctness for complex multi-node interactions.

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 →