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 maxGeohashLength bytes.
  • eventJSON: The complete signed Nostr event including id, pubkey, created_at, kind, tags, content, and sig. The implementation caps this at maxEventJSONBytes (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:

  1. Structural decoding: Calls NostrCarrierPacket.decode(payload) to extract fields.
  2. Size validation: Enforces maxEventJSONBytes limits to prevent resource exhaustion.
  3. 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 for toGateway or toBridge packets targeting specific peers.
  • broadcastNostrCarrier(_:): Floods the mesh with fromGateway or fromBridge packets.

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 .toGateway packets for uplink, while gateways inject .fromGateway packets for downlink.
  • Bridge nodes extend coverage to isolated mesh islands using BridgeService with .toBridge and .fromBridge directions.
  • 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:

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 →