# How Bitchat Enables Deterministic Multi-Node Testing Without Physical Hardware

> Learn how Bitchat's pure Swift simulator enables deterministic multi-node testing without physical hardware by replacing CoreBluetooth with an in-memory mesh of sans-I/O engines.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: how-to-guide
- Published: 2026-08-09

---

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

```swift
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:

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/BLE-ARCHITECTURE-V3.md)
- **SimulatedMesh** wires multiple engine instances via `_test_onOutboundPacket` taps and lock-protected delivery queues in [`SimulatedMesh.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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.