How NostrCarrier Packets Bridge BitChat’s Mesh Network with the Internet
NostrCarrier packets are signed, TLV-encoded payloads that traverse BitChat’s Bluetooth LE mesh as ordinary traffic, enabling gateway nodes to verify signatures and relay messages between offline mesh devices and public Nostr relays.
BitChat creates a peer-to-peer mesh network over local Bluetooth LE links, intentionally isolating communication from the internet. To enable mesh-only devices to exchange messages with the global Nostr network without breaking the decentralized model, the app uses NostrCarrier packets—a specialized protocol defined in bitchat/Protocols/NostrCarrierPacket.swift. According to the permissionlesstech/bitchat source code, these packets wrap cryptographically signed Nostr events in a transport-agnostic format that mesh peers treat as standard data, while designated gateways interpret them as bridging instructions.
Packet Structure and TLV Encoding
The NostrCarrierPacket struct defines a binary format optimized for constrained mesh networks. It uses Type-Length-Value (TLV) encoding with 2-byte big-endian length fields.
Core Fields
Each carrier contains three critical components:
- direction: An enum value (
toGateway,fromGateway,toBridge,fromBridge) indicating routing intent—whether the packet is a directed request toward a gateway or a broadcast originating from one. - geohash: A 12-character location identifier serving as the Nostr "room" name, limited to
maxGeohashLengthbytes. - eventJSON: The complete signed Nostr event including
id,pubkey,created_at,kind,tags,content, andsig. The implementation caps this atmaxEventJSONBytes(16 KB) to prevent mesh congestion.
The event data remains unencrypted because Nostr events are public by design, but the embedded Schnorr signature ensures authenticity.
Serialization Logic
The encode() method (lines 88–102) constructs the TLV byte stream by writing each field type, length, and value sequentially. Conversely, decode(_:) (lines 5–44) iterates over TLV blocks to reconstruct the packet. The decoder is intentionally tolerant—unknown TLV types are skipped (lines 31–33)—ensuring forward compatibility as the protocol evolves.
// Encoding a packet for mesh broadcast
let packet = NostrCarrierPacket(
direction: .toGateway,
geohash: "u4pruy",
event: signedEvent
)!
let payload = packet.encode()!
Bridging Flow: Mesh to Internet
When a mesh-only device needs to publish to Nostr, it creates a carrier packet with direction .toGateway and broadcasts it via BLE.
Gateway Reception and Verification
Upon receiving the payload, GatewayService.handleMeshCarrier(_:from:directedTo:) (lines 164–172) executes a strict validation pipeline:
- Structural decoding: Calls
NostrCarrierPacket.decode(payload)to extract fields. - Size validation: Enforces
maxEventJSONByteslimits to prevent resource exhaustion. - Signature verification: Invokes
event.isValidSignature()using Schnorr cryptography—gateways reject any packet with invalid signatures before relaying.
Only after these checks does the gateway call handleUplinkDeposit to publish the event to configured Nostr relays, linking the offline geohash-based chat room to the global internet.
// Inside GatewayService.swift (lines 164-172)
func handleMeshCarrier(_ payload: Data, from peerID: PeerID, directedToUs: Bool) {
guard let carrier = NostrCarrierPacket.decode(payload),
let event = carrier.event(),
event.isValidSignature() else { return }
if carrier.direction == .toGateway {
publish(event: event, geohash: carrier.geohash) // handleUplinkDeposit
}
}
Bridging Flow: Internet to Mesh
For inbound traffic, gateways monitor Nostr relays for events matching subscribed geohashes. When a relevant event arrives, the gateway wraps it in a NostrCarrierPacket with direction .fromGateway and calls handleDownlinkBroadcast.
This broadcasts the payload to all mesh peers via BLEService.broadcastNostrCarrier(_:), injecting internet-originated messages back into the local Bluetooth mesh. Mesh nodes receive these as standard BLE packets, decoding them through the same NostrCarrierPacket.decode path used for outbound traffic.
Cross-Island Bridging with BridgeService
When mesh networks form isolated "islands" without direct internet gateways, bridge nodes enable inter-island communication. BridgeService.handleMeshCarrier(_:from:directedTo:) (lines 516–523) mirrors the gateway logic but uses .toBridge and .fromBridge directions.
A bridge node forwards events between disconnected mesh segments and the internet, acting as a relay chain. This architecture allows "island-to-island" messaging where one island’s gateway publishes to Nostr, and a distant island’s bridge pulls the event down into its local mesh.
BLE Transport Implementation
The mesh layer treats Nostr carriers as opaque data blobs. BLEService abstracts the transport details through two helper methods:
sendNostrCarrier(_:to:): Directed transmission fortoGatewayortoBridgepackets targeting specific peers.broadcastNostrCarrier(_:): Floods the mesh withfromGatewayorfromBridgepackets.
Both methods set MessageType.nostrCarrier (0x28) in the BLE packet header (lines 6115–6138) and trigger the onNostrCarrierPacket callback, which routes data to the appropriate service (GatewayService or BridgeService).
Mesh initiation occurs in ChatViewModelBootstrapper.swift (lines 353–356), where user messages trigger broadcastNostrCarrier(payload) after packet encoding.
Security and Trust Model
NostrCarrier packets enforce security through cryptographic verification rather than transport encryption. Because Nostr events are publicly visible, the carrier transmits the eventJSON in plaintext but includes the original sig field.
Gateways validate integrity via structurallyValidEvent checks (lines 444–465), ensuring:
- The event size complies with mesh limits.
- The Schnorr signature mathematically proves the event originated from the claimed public key.
This prevents mesh spoofing; only the legitimate per-geohash identity can inject events, even though the BLE transport is promiscuous.
Summary
- NostrCarrier packets wrap signed Nostr events in a TLV format defined in
bitchat/Protocols/NostrCarrierPacket.swift, adding direction and geohash metadata. - Bidirectional bridging occurs via
GatewayService: mesh nodes broadcast.toGatewaypackets for uplink, while gateways inject.fromGatewaypackets for downlink. - Bridge nodes extend coverage to isolated mesh islands using
BridgeServicewith.toBridgeand.fromBridgedirections. - Security relies on Schnorr signatures verified by gateways before relaying, preventing unauthorized injection into the Nostr network.
- BLE transport uses
MessageType.nostrCarrier(0x28) with 16 KB event size limits to ensure mesh stability.
Frequently Asked Questions
What is the maximum size of a NostrCarrier packet?
The implementation limits the Nostr event JSON to 16 KB (maxEventJSONBytes) and the geohash to 12 bytes (maxGeohashLength), keeping the total carrier payload under 16 KB plus TLV overhead. This prevents Bluetooth LE congestion and ensures compatibility with mesh routing.
Why doesn't the carrier encrypt the Nostr event?
Encryption is unnecessary because Nostr events are public by design. The carrier relies on the embedded Schnorr signature to prove authenticity. Gateways verify event.isValidSignature() before relaying, ensuring only valid events enter the public Nostr network, while the content remains readable to all mesh participants consistent with Nostr’s public timeline model.
How does a mesh-only device find a gateway?
Mesh nodes broadcast NostrCarrierPacket with direction .toGateway using BLEService.broadcastNostrCarrier. Any gateway peer within BLE range receives the broadcast, decodes the packet, and processes the request. There is no discovery protocol—the mesh flooding ensures reachability to any local gateway without central coordination.
Can malicious nodes inject fake messages into the mesh?
No. While any peer can broadcast BLE packets, gateways and bridges verify the Schnorr signature inside NostrCarrierPacket.event() before acting. Invalid signatures result in immediate rejection at GatewayService.structurallyValidEvent. This cryptographic binding ensures only the holder of the private key corresponding to the Nostr public key can inject events, preventing spoofing even on an open mesh.
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 →