# How to Test Mesh Networking Behavior in BitChat: A Complete Guide for Developers

> Test BitChat mesh networking behavior effectively using the SimulatedMesh harness. Developers can validate BLEService engines in memory, no hardware needed. Read the complete guide.

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

---

**BitChat provides a deterministic test harness called `SimulatedMesh` that runs real `BLEService` engines in memory, enabling developers to validate mesh networking behavior without physical Bluetooth hardware.**

Testing mesh networking in BitChat requires exercising complex distributed behaviors—peer discovery, encrypted handshakes, message relay, and identity rotation—while maintaining test speed and reproducibility. The `SimulatedMesh` harness in the `permissionlesstech/bitchat` repository solves this by creating virtual nodes that execute the production mesh stack against in-memory links. This guide explains how to use this system to validate every layer of BitChat's mesh protocol.

## Understanding the SimulatedMesh Test Architecture

The core testing infrastructure centers on three interconnected components that together provide full-stack fidelity without CoreBluetooth dependencies.

### SimulatedMesh: The Deterministic Harness

[[`SimulatedMesh.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMesh.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Simulation/SimulatedMesh.swift) implements the test orchestrator. It manages collections of virtual nodes, wires them together with logical links, and controls time progression through manual schedulers. The harness buffers outbound packets, advances each node's `BLEEngineManualScheduler` individually, and pumps deliveries until the network reaches quiescence.

### BLEService: The Engine Under Test

Each virtual node runs an unmodified [[`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/BLEService.swift) instance—the same engine used in production. `SimulatedMesh` injects packets through the `_test_ingestFrame` hook, bypassing Bluetooth radio layers while preserving all cryptographic, routing, and state machine logic.

### TransportEventCapture: Verification Infrastructure

The `TransportEventCapture` class (defined in [[`SimulatedMeshTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMeshTests.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Simulation/SimulatedMeshTests.swift#L302-L330)) implements `TransportEventDelegate` to record `.publicMessageReceived` events. This enables assertions that messages actually traversed the mesh and arrived at intended destinations.

## Running Your First Mesh Network Test

Testing mesh networking behavior in BitChat follows a six-step pattern: create nodes, connect them with logical edges, trigger announces, advance time, pump traffic, and capture events for verification.

### Step 1: Create and Connect Nodes

```swift
let mesh = SimulatedMesh()
let alice = mesh.addNode(nickname: "alice")
let bob = mesh.addNode(nickname: "bob")
mesh.connect(0, 1)  // Establishes bidirectional logical link

```

### Step 2: Trigger Peer Discovery

```swift
mesh.announceAll()  // Forces each node to broadcast signed announce packets

```

### Step 3: Verify Link Binding

```swift
#expect(bob.service._test_centralBinding(mesh.linkUUID(from: 0, at: 1)) == alice.service.myPeerID)
#expect(alice.service._test_centralBinding(mesh.linkUUID(from: 1, at: 0)) == bob.service.myPeerID)

```

The `linkUUID(from:at:)` helper generates stable identifiers that act as **central link identifiers** for the mesh protocol. These strings enable deterministic routing without actual Bluetooth peripheral/central role negotiation.

## Testing Encrypted Mesh Sessions

Once basic connectivity passes, the next priority is validating **Noise XX handshakes** that establish end-to-end encryption across mesh hops.

```swift
let mesh = SimulatedMesh()
let a = mesh.addNode(nickname: "alice")
let b = mesh.addNode(nickname: "bob")
mesh.connect(0, 1)
mesh.announceAll()

mesh.settleUntil {
    a.service.canDeliverSecurely(to: b.service.myPeerID) &&
    b.service.canDeliverSecurely(to: a.service.myPeerID)
}

#expect(a.service.canDeliverSecurely(to: b.service.myPeerID))
#expect(b.service.canDeliverSecurely(to: a.service.myPeerID))

```

The `settleUntil(_:)` convenience method repeatedly pumps traffic and advances time until the provided condition becomes true or a safety timeout fires. This pattern replaces brittle `sleep()` calls with deterministic progress.

*Source: [[`SimulatedMeshTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMeshTests.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Simulation/SimulatedMeshTests.swift), `noiseSessionEstablishesEndToEnd` (lines 31–47)*

## Validating Message Relay and TTL Behavior

Mesh networking tests must verify that messages propagate through intermediate nodes without exceeding [`TransportConfig.messageTTLDefault`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/TransportConfig.swift).

```swift
let mesh = SimulatedMesh()
let a = mesh.addNode(nickname: "alice")
_ = mesh.addNode(nickname: "bob")  // index 1: relay node
let c = mesh.addNode(nickname: "carol")

mesh.connect(0, 1)  // Alice-Bob link
mesh.connect(1, 2)  // Bob-Carol link
mesh.announceAll()
mesh.advanceTime(by: 2)

let capture = TransportEventCapture()
c.service.eventDelegate = capture

a.service.sendMessage("hello line", mentions: [])
mesh.pump()                       // Bob receives, then relays to Carol
mesh.advanceTime(by: 2)           // Release relay jitter timers
mesh.advanceTime(by: 2)

let arrived = await capture.drainedPublicMessageCount(content: "hello line") == 1
#expect(arrived)
#expect(mesh.deliveredFrameCount - baseline <= 12)  // Storm bound checked

```

Key assertions here confirm:
- **Delivery guarantee**: Carol receives the message despite no direct link to Alice
- **Storm control**: Total frames stay within reasonable bounds (12 in this test)

*Source: [[`SimulatedMeshTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMeshTests.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Simulation/SimulatedMeshTests.swift), `publicMessageRelaysAcrossLineTopologyWithinTTLBudget` (lines 50–76)*

## Testing Duplicate Suppression

Mesh protocols must **deduplicate flood traffic** to prevent exponential amplification. BitChat's mesh layer tracks seen message identifiers and suppresses retransmission.

```swift
let mesh = SimulatedMesh()
let a = mesh.addNode(nickname: "alice")
let b = mesh.addNode(nickname: "bob")
mesh.connect(0, 1)
mesh.announceAll()

let capture = TransportEventCapture()
b.service.eventDelegate = capture

let packet = BitchatPacket(
    type: MessageType.message.rawValue,
    senderID: Data(hexString: a.service.myPeerID.id) ?? Data(),
    recipientID: nil,
    timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
    payload: Data("flooded".utf8),
    signature: nil,
    ttl: TransportConfig.messageTTLDefault
)
let signed = a.service.signPacketForBroadcast(packet)
let link = BLEIngressLinkID.central(mesh.linkUUID(from: 0, at: 1))

// Simulate malicious or pathological flooding
for _ in 0..<8 { b.service._test_ingestFrame(signed, link: link) }

mesh.pump()
mesh.advanceTime(by: 2)

let deliveredOnce = await capture.drainedPublicMessageCount(content: "flooded") == 1
#expect(deliveredOnce)  // Deduplication verified

```

The test directly injects eight identical frames via `_test_ingestFrame` and confirms only one delivery event fires.

*Source: [[`SimulatedMeshTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMeshTests.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Simulation/SimulatedMeshTests.swift), `duplicateFloodIsDeliveredOnce` (lines 89–108)*

## Simulating Panic Rotation with Duplicate Links

Advanced mesh networking tests cover **identity rotation**—when a node resets its cryptographic identity due to security events. BitChat's mesh must heal all associated links without ghost peer entries.

```swift
let mesh = SimulatedMesh()
let a = mesh.addNode(nickname: "alice")
let b = mesh.addNode(nickname: "bob")
mesh.connectDuplicateLinks(0, 1)  // Two parallel central links
mesh.announceAll()

let centralLink = BLEIngressLinkID.central(mesh.linkUUID(from: 1, at: 0))
let duplicateLink = BLEIngressLinkID.central(mesh.duplicateLinkUUID(from: 1, at: 0))

let oldBobID = b.service.myPeerID

// Both links initially bind to same identity
#expect(a.service._test_linkBinding(centralLink) == oldBobID)
#expect(a.service._test_linkBinding(duplicateLink) == oldBobID)

// Trigger panic reset on Bob
b.service.suspendForPanicReset()
b.service.resetIdentityForPanic(currentNickname: "anon", restartServices: false)
b.service.completePanicReset(restartServices: false)
mesh.pump()

// Force re-announce and wait for old identity expiration
mesh.forceAnnounce(from: 1)
mesh.settleUntil { !a.service._test_knownPeerIDs().contains(oldBobID) }

// Both links now bind to new identity, old identity gone
#expect(a.service._test_linkBinding(centralLink) != oldBobID)
#expect(a.service._test_linkBinding(duplicateLink) != oldBobID)

```

The `connectDuplicateLinks` helper creates realistic multi-path topologies. This test validates that **panic rotation correctly clears all binding state** across redundant links.

*Source: [[`SimulatedMeshTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMeshTests.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Simulation/SimulatedMeshTests.swift), `duplicateLinkPanicRotationLeavesNoGhostAndHealsBothLinks` (lines 51–91)*

## Key API Reference for Mesh Testing

| Method | Purpose | Location |
|--------|---------|----------|
| `addNode(nickname:)` | Creates virtual node with `BLEService` instance | [`SimulatedMesh.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMesh.swift) |
| `connect(_:_:)` | Establishes single logical link between node indices | [`SimulatedMesh.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMesh.swift) |
| `connectDuplicateLinks(_:_:)` | Creates parallel links for multi-path testing | [`SimulatedMesh.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMesh.swift) |
| `announceAll()` | Triggers signed announce broadcast from all nodes | [`SimulatedMesh.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMesh.swift) |
| `forceAnnounce(from:)` | Triggers announce from specific node index | [`SimulatedMesh.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMesh.swift) |
| `advanceTime(by:)` | Steps manual scheduler by seconds | [`SimulatedMesh.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMesh.swift) |
| `pump()` | Delivers buffered packets until quiescence | [`SimulatedMesh.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMesh.swift) |
| `settleUntil(_:)` | Pumps until condition true or timeout | [`SimulatedMesh.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMesh.swift) |
| `linkUUID(from:at:)` | Generates stable central link identifier | [`SimulatedMesh.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMesh.swift) |
| `duplicateLinkUUID(from:at:)` | Generates parallel link identifier | [`SimulatedMesh.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMesh.swift) |
| `_test_ingestFrame(_:link:)` | Injects frame into `BLEService` test hook | [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) |
| `_test_centralBinding(_:)` | Returns `PeerID` bound to central link | [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) |
| `_test_linkBinding(_:)` | Returns `PeerID` for any link type | [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) |
| `_test_knownPeerIDs()` | Returns set of currently known peers | [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) |

## Summary

Testing mesh networking behavior in BitChat relies on the `SimulatedMesh` deterministic harness that executes production `BLEService` engines without Bluetooth hardware. Key takeaways:

- **Full-stack fidelity**: Virtual nodes run unmodified production code through `_test_ingestFrame` injection
- **Deterministic control**: Manual schedulers and explicit time advancement eliminate flaky timing dependencies
- **Comprehensive coverage**: Tests span peer binding, Noise encryption, multi-hop relay, deduplication, and identity rotation
- **Fast iteration**: In-memory execution completes complex mesh scenarios in milliseconds

The test infrastructure lives in [`bitchatTests/Simulation/`](https://github.com/permissionlesstech/bitchat/tree/main/bitchatTests/Simulation) with examples demonstrating every major mesh behavior pattern.

## Frequently Asked Questions

### What makes SimulatedMesh different from mocking Bluetooth APIs?

`SimulatedMesh` exercises the actual `BLEService` production implementation—including its cryptography, state machines, and routing logic—by injecting frames through a test hook rather than replacing the engine with mocks. This provides behavioral guarantees that mock-based tests cannot match.

### How does advanceTime handle timer-based jitter in mesh routing?

`advanceTime(by:)` steps each node's `BLEEngineManualScheduler` individually, releasing timer callbacks exactly as they would fire in production. The `pump()` method then processes any newly queued work, creating deterministic progress through time-sensitive behaviors like relay jitter and retry backoffs.

### Can SimulatedMesh test topology changes mid-scenario?

Yes. Tests can call `connect`, `disconnect`, or `connectDuplicateLinks` at any point, then `pump()` to propagate the changes. The panic rotation example demonstrates dynamic identity changes—similar patterns work for link failures and network partitions.

### What TTL values should mesh message tests use?

Use [`TransportConfig.messageTTLDefault`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/TransportConfig.swift) for realistic relay bounds, or explicitly set `ttl` fields in crafted `BitchatPacket` instances to test expiration edge cases.