BitChat TLV Packet Encoding: How AnnouncementPacket and Protocols Structure Network Data
BitChat encodes all network messages using a compact Type-Length-Value (TLV) scheme where each field serializes as a 1-byte type identifier, 1-byte length field, and variable-length value payload, enabling forward-compatible peer-to-peer communication.
The permissionlesstech/bitchat repository implements a mesh networking protocol that relies on consistent data serialization across its discovery, authentication, and messaging layers. Every packet type—including AnnouncementPacket, AuthenticatedPeerStatePacket, and PrivateMessagePacket—uses this TLV packet encoding format to ensure compact wire representation and graceful handling of unknown fields.
Understanding the TLV Structure
Each TLV record follows a strict three-part byte sequence:
- Type — 1 byte (
UInt8): Identifies what the value represents (e.g., nickname, public key). - Length — 1 byte (
UInt8): Specifies the number of bytes in the value field (0–255). - Value — Length bytes: The actual payload data.
The complete packet is formed by concatenating multiple TLV records. Optional fields are omitted entirely rather than encoded as zero-length values, and decoders skip unknown types by reading the length field and advancing the pointer, ensuring forward compatibility.
AnnouncementPacket TLV Specification
AnnouncementPacket handles peer discovery and presence broadcasting. The TLV type identifiers are defined in the AnnouncementPacket.TLVType enum located at lines 33–41 of bitchat/Protocols/Packets.swift.
| TLV Type | Field | Description |
|---|---|---|
0x01 |
nickname | UTF-8 encoded display name |
0x02 |
noisePublicKey | Curve25519 public key |
0x03 |
signingPublicKey | Ed25519 signing public key |
0x04 |
directNeighbors | Array of 8-byte peer IDs |
0x05 |
capabilities | PeerCapabilities bitfield |
0x06 |
bridgeGeohash | UTF-8 geohash string (≤12 bytes) |
The encoder (lines 42–94) builds the byte array by reserving capacity for mandatory fields first, then appending optional TLVs only if present. The decoder (lines 96–157) walks the stream sequentially, populating struct fields based on the type identifier and ignoring unrecognized types.
AuthenticatedPeerStatePacket TLV Format
This authentication packet prepends a version byte (0x01 for the current format) before the TLV stream to allow future protocol revisions. The version check occurs first; mismatched versions result in immediate packet rejection.
Following the version byte, the TLV stream uses these identifiers (defined at lines 64–82):
| TLV Type | Field | Description |
|---|---|---|
0x01 |
capabilities | Current peer capabilities |
0x02 |
signingPublicKey | 32-byte Ed25519 public key |
The encoding logic resides at lines 84–97, while decoding operates at lines 99–140 in bitchat/Protocols/Packets.swift.
PrivateMessagePacket TLV Structure
For direct messaging, PrivateMessagePacket employs a minimal TLV schema defined at lines 46–49:
| TLV Type | Field | Description |
|---|---|---|
0x00 |
messageID | UTF-8 string UUID |
0x01 |
content | UTF-8 message body (max 255 bytes) |
Both fields are optional in the protocol sense, though typically present. The encoder (lines 51–68) writes each TLV only if the corresponding property is non-nil, and the decoder (lines 70–95) reconstructs the message by parsing the type and length headers before extracting the UTF-8 payload.
Implementation Details in Packets.swift
The central bitchat/Protocols/Packets.swift file contains the canonical implementations for all TLV operations. When encoding, each packet type iterates through its optional properties, calculating total buffer size beforehand to minimize reallocations, then writes [type][length][value] sequences to the buffer.
Decoding reverses this process: the parser reads the type byte, checks if it recognizes the identifier, reads the length byte, then either copies the value bytes into the appropriate struct field or skips them if the type is unknown. This skip mechanism operates entirely on the length field, requiring no knowledge of the value's internal structure.
Practical Code Examples
Constructing an Announcement Packet
let packet = AnnouncementPacket(
nickname: "alice",
noisePublicKey: noiseKeyData,
signingPublicKey: signingKeyData,
directNeighbors: [peerID1, peerID2],
capabilities: .init(capabilities: [.audio, .video]),
bridgeGeohash: "u4pruydqqvj"
)
guard let encoded = packet.encode() else { fatalError("Encoding failed") }
The encode() method implements the TLV stream construction described above, processing each field in type order.
Decoding an Announcement Packet
if let decoded = AnnouncementPacket.decode(from: receivedData) {
print("Nick: \(decoded.nickname)")
// Access other parsed fields...
} else {
print("Invalid announcement")
}
Using the Authenticated Peer State Packet
let authPacket = AuthenticatedPeerStatePacket(
capabilities: .init(capabilities: [.text]),
signingPublicKey: signingKeyData
)
let data = authPacket.encode()!
let restored = AuthenticatedPeerStatePacket.decode(from: data)
Note that encode() automatically prepends the version byte before the TLV records.
Sending a Private Message
let msg = PrivateMessagePacket(
messageID: UUID().uuidString,
content: "Hello, world!"
)
let payload = msg.encode()!
Summary
- BitChat uses a standardized TLV packet encoding across
AnnouncementPacket,AuthenticatedPeerStatePacket, andPrivateMessagePacketto ensure interoperability. - Each TLV record consists of a 1-byte type identifier, 1-byte length field, and a value payload limited to 255 bytes.
- Forward compatibility is achieved by omitting optional fields entirely and having decoders skip unknown TLV types based on the length field.
AuthenticatedPeerStatePacketuniquely prefixes its TLV stream with a version byte (0x01) to support future breaking changes in authentication format.- All encoding and decoding logic is centralized in
bitchat/Protocols/Packets.swift, with comprehensive test coverage inbitchatTests/Protocols/PacketsTests.swiftand consumption bybitchat/Sync/GossipSyncManager.swift.
Frequently Asked Questions
What is the maximum size of a single TLV value in BitChat?
The length field is defined as a single unsigned byte (UInt8), which limits each individual TLV value to 255 bytes. Applications requiring larger data payloads must split data across multiple TLV records or handle fragmentation at a higher protocol layer.
How does BitChat maintain compatibility when adding new protocol fields?
The decoder implementations in bitchat/Protocols/Packets.swift (specifically lines 96–157 for AnnouncementPacket) skip unknown TLV types by reading the length field and advancing the buffer pointer accordingly. This allows newer clients to introduce additional TLV type identifiers without breaking older clients that simply ignore unrecognized types.
Why does AuthenticatedPeerStatePacket include a version byte while AnnouncementPacket does not?
AuthenticatedPeerStatePacket includes an explicit version byte (0x01) preceding the TLV stream (as implemented at lines 84–97) to accommodate potential future breaking changes in authentication cryptography or structure. AnnouncementPacket relies solely on the extensible TLV format and type ordering for compatibility, as its field set has remained stable according to the source analysis.
Where are the TLV encoding and decoding unit tests located?
Comprehensive round-trip tests verifying that encoding followed by decoding preserves data integrity for all packet types reside in bitchatTests/Protocols/PacketsTests.swift. These tests validate the TLV wire format against the specifications defined in bitchat/Protocols/Packets.swift.
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 →