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, and PrivateMessagePacket to 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.
  • AuthenticatedPeerStatePacket uniquely 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 in bitchatTests/Protocols/PacketsTests.swift and consumption by bitchat/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:

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 →