BitChat Packet Types: Complete Guide to Message Encoding and Processing
BitChat uses 17 distinct packet types—from 0x01 (announce) to 0x2C (announceV2)—each with single-byte identifiers processed through a binary protocol that handles compression, routing, encryption, and signature verification.
The BitChat mesh messaging protocol defines a compact wire-level format for decentralized peer-to-peer communication. This article examines the packet type system implemented in the permissionlesstech/bitchat repository, covering how messages are constructed, encoded, routed, and processed across the network.
Core Packet Types in BitChat
All packet type identifiers are defined in MessageType.swift as a Swift enumeration. Each case maps to a single byte value used by the binary encoder.
Public and Presence Messages
These packet types handle basic mesh participation and public chat:
-
announce(0x01) — Peer advertises presence with a nickname. Encoded as plain-text; processed by the announce coordinator to build the local peer directory. -
message(0x02) — Standard public chat message. Sent unencrypted; payload passed directly to the chat view model for display. -
leave(0x03) — Peer departure notification. Triggers cleanup of peer state in the local mesh table. -
announceV2(0x2C) — Identity-free presence for rotating peer IDs. Contains epoch, recognition tags, and capability flags; parsed according to [docs/PEER-ID-ROTATION.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/PEER-ID-ROTATION.md).
Encrypted and Key-Management Packets
These types implement the Noise protocol and cryptographic key exchange:
-
noiseHandshake(0x10) — Noise protocol handshake (initiator or responder). Processed by the Noise handshake state machine to establish ephemeral session keys. -
noiseEncrypted(0x11) — All encrypted payloads including messages and receipts. Decrypted using the established Noise session key before further processing. -
prekeyBundle(0x24) — Batch of one-time pre-keys gossiped between peers. Added to the pre-key store for future session establishment.
Synchronization and Routing Packets
Mesh maintenance and state synchronization use these types:
-
courierEnvelope(0x04) — Store-and-forward envelope for trusted peers. Decoded by the courier service, then forwarded toward its destination. -
requestSync(0x21) — Local filter-based sync request. Handled by the sync manager; not routed through the mesh. -
fragment(0x20) — Fragment of a large message. Reassembled by the fragmentation buffer before higher-level processing. -
ping(0x26) — Echo request containing nonce and origin TTL. Target node returns apongresponse. -
pong(0x27) — Echo reply with matching nonce and TTL. Used to measure round-trip latency between peers.
Content and Application Packets
Specialized payload types for file transfer, voice, and external protocols:
-
fileTransfer(0x22) — Binary file, audio, or image payload. Decoded by the file-transfer service; may be zlib-compressed. -
boardPost(0x23) — Signed geohash bulletin-board post or tombstone. Verified with sender's public key, then stored locally. -
groupMessage(0x25) — Group-encrypted broadcast with cleartext group ID and ChaChaPoly body. Decrypted with group key after group ID lookup. -
nostrCarrier(0x28) — Signed Nostr event ferried between mesh-only and internet-gateway peers. Decoded by the Nostr bridge and relayed to appropriate Nostr relays. -
voiceFrame(0x29) — Push-to-talk burst packet for ephemeral broadcast. Handled by the live-voice subsystem; not gossip-synced or stored.
How BitChat Packets Are Processed
The packet lifecycle follows four stages: construction, binary encoding, network transmission, and decoding with type-specific handling.
1. Packet Construction
A BitchatPacket is instantiated through its initializer in BitchatPacket.swift (source):
let packet = BitchatPacket(
type: MessageType.message.rawValue, // 0x02
senderID: myPeerID,
recipientID: nil, // nil for broadcast
timestamp: UInt64(Date().timeIntervalSince1970 * 1_000_000),
payload: Data("Hello, mesh!".utf8),
signature: nil,
ttl: 8
)
Key fields:
version— Protocol version (1 or 2)type— MessageType raw value (single byte)senderID/recipientID— 8-byte peer identifierstimestamp— Microseconds since Unix epochpayload— Variable-length message datasignature— Optional 64-byte Ed25519 signaturettl— Time-to-live hop counterroute— Optional path array (v2 only)isRSR— Re-send-request flag (excluded from signing)
2. Binary Encoding
The BinaryProtocol.encode(_:padding:) method (source) builds the wire format through these steps:
- Validate version (1 or 2)
- Compress payload if
CompressionUtil.shouldCompressreturns true - Assemble header:
version,type,ttl, 8-byte timestamp, flags - Set flag bits for: recipient present, signature present, compressed, route present, RSR
- Write length field (16-bit for v1, 32-bit for v2)
- Append fixed fields:
senderID, optionalrecipientID - If routed (v2): write hop count and each 8-byte hop
- If compressed: prepend original payload size
- Append payload and optional signature
- Optionally pad to optimal block size via
MessagePadding
// Encode with padding for optimal transmission
guard let binary = packet.toBinaryData(padding: true) else {
fatalError("Encoding failed")
}
// Send `binary` via BLE, LoRa, or other transport
3. Binary Decoding
The BinaryProtocol.decode(_:) method (source) reverses encoding:
- Attempts raw decode; on failure strips padding and retries
- Reads header and extracts flag bits
- Determines which optional sections are present
- Reads route array if indicated
- Decompresses payload when compression flag is set
- Reconstructs
BitchatPacketinstance
// On receiving side
guard let received = BitchatPacket.from(binaryData) else {
fatalError("Decoding failed")
}
4. Type-Specific Processing
After decoding, the packet type determines handling:
switch MessageType(rawValue: received.type) {
case .message:
let text = String(data: received.payload, encoding: .utf8)!
print("Received: \(text)")
case .noiseEncrypted:
// Decrypt with established Noise session
let plaintext = noiseSession.decrypt(received.payload)
case .voiceFrame:
// Ephemeral: play immediately, do not store
audioEngine.play(received.payload)
case .nostrCarrier:
// Bridge to Nostr relay
nostrBridge.relay(received.payload)
default:
break
}
Routing, Security, and Special Handling
TTL and Mesh Routing
Each hop decrements the ttl field. Packets with ttl == 0 are discarded. In protocol v2, the route array tracks the exact path; routers strip the first hop before forwarding.
Signature Verification
When hasSignature flag is set, the payload is verified against the embedded 64-byte Ed25519 signature. The isRSR flag is excluded from signature generation—use toBinaryDataForSigning() to produce the signed representation.
Compression and Padding
CompressionUtil applies zlib compression when beneficial. MessagePadding aligns packets to optimal block sizes for the underlying transport, improving throughput on BLE and LoRa links.
Fragmentation
Large messages exceeding transport MTU are split into fragment (0x20) packets. The fragmentation buffer reassembles them before higher-level processing.
Complete Example: Sending and Receiving
import BitFoundation
// Build and send a public message
func sendChatMessage(text: String, to mesh: MeshManager) {
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: myPeerID,
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1_000_000),
payload: Data(text.utf8),
signature: nil,
ttl: 8
)
guard let binary = packet.toBinaryData(padding: true) else { return }
mesh.broadcast(binary)
}
// Process incoming packets
func handlePacket(_ data: Data) {
guard let packet = BitchatPacket.from(data),
let type = MessageType(rawValue: packet.type) else { return }
switch type {
case .announce:
peerDirectory.update(from: packet)
case .message:
display(ChatMessage(packet))
case .ping:
respondWithPong(to: packet)
default:
routeOrProcess(packet)
}
}
Key Implementation Files
| File | Purpose | Link |
|---|---|---|
MessageType.swift |
Packet type enumeration with all 17 identifiers | View source |
BitchatPacket.swift |
Core data structure with toBinaryData and from helpers |
View source |
BinaryProtocol.swift |
Low-level encoder/decoder, flag handling, compression, routing | View source |
docs/PEER-ID-ROTATION.md |
Specification for announceV2 rotating-ID format |
View docs |
Summary
- 17 packet types cover all BitChat functionality, from basic presence (
0x01) to Nostr bridging (0x28) and rotating peer IDs (0x2C). - Single-byte identifiers enable compact binary encoding with minimal overhead.
BinaryProtocol.swifthandles version negotiation, compression, routing, padding, and flag management transparently.- Noise protocol integration provides encrypted channels via
noiseHandshake(0x10) andnoiseEncrypted(0x11). - Type-specific processing occurs after decode: voice frames are ephemeral, fragments are reassembled, and Nostr events are bridged to external relays.
Frequently Asked Questions
How does BitChat handle packet versioning?
BitChat supports protocol versions 1 and 2. The version field in the header determines field sizes—v1 uses 16-bit length fields while v2 uses 32-bit lengths and adds optional routing arrays. The BinaryProtocol encoder validates the version and sets flags accordingly.
What is the difference between announce and announceV2?
The original announce (0x01) includes a persistent nickname tied to a stable peer ID. announceV2 (0x2C) enables privacy-preserving presence without fixed identity, using epoch-based rotation, recognition tags, and capability flags—documented in docs/PEER-ID-ROTATION.md.
Are BitChat packets always signed?
No. Signatures are optional and controlled by the hasSignature flag. When present, the 64-byte Ed25519 signature covers all packet fields except the isRSR flag. Plain public messages and voice frames typically omit signatures for lower overhead, while bulletin-board posts and pre-key bundles require authentication.
How does the fragmentation system work?
Messages exceeding the transport MTU are automatically split by the sender into multiple fragment (0x20) packets. Each fragment carries sequence information for reassembly. The receiving peer buffers fragments until the complete message is reconstructed, then processes the reassembled payload according to the original message type.
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 →