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
Step 3: Verify Link Binding
#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)
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.
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_ingestFrameinjection - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →