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

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/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/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/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

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

mesh.announceAll()  // Forces each node to broadcast signed announce packets
#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.

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/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.

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/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.

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/bitchatTests/Simulation/SimulatedMeshTests.swift), duplicateFloodIsDeliveredOnce (lines 89–108)

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.

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/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
connect(_:_:) Establishes single logical link between node indices SimulatedMesh.swift
connectDuplicateLinks(_:_:) Creates parallel links for multi-path testing SimulatedMesh.swift
announceAll() Triggers signed announce broadcast from all nodes SimulatedMesh.swift
forceAnnounce(from:) Triggers announce from specific node index SimulatedMesh.swift
advanceTime(by:) Steps manual scheduler by seconds SimulatedMesh.swift
pump() Delivers buffered packets until quiescence SimulatedMesh.swift
settleUntil(_:) Pumps until condition true or timeout SimulatedMesh.swift
linkUUID(from:at:) Generates stable central link identifier SimulatedMesh.swift
duplicateLinkUUID(from:at:) Generates parallel link identifier SimulatedMesh.swift
_test_ingestFrame(_:link:) Injects frame into BLEService test hook BLEService.swift
_test_centralBinding(_:) Returns PeerID bound to central link BLEService.swift
_test_linkBinding(_:) Returns PeerID for any link type BLEService.swift
_test_knownPeerIDs() Returns set of currently known peers 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/ 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 for realistic relay bounds, or explicitly set ttl fields in crafted BitchatPacket instances to test expiration edge cases.

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 →