# BitChat TLV Packet Encoding: How AnnouncementPacket and Protocols Structure Network Data

> Discover BitChat's TLV packet encoding format for network data. Learn how AnnouncementPacket and protocols structure messages using efficient type-length-value serialization for forward-compatible P2P communication.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: internals
- Published: 2026-07-30

---

**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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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

```swift
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

```swift
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

```swift
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

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/Packets.swift), with comprehensive test coverage in [`bitchatTests/Protocols/PacketsTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Protocols/PacketsTests.swift) and consumption by [`bitchat/Sync/GossipSyncManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Protocols/PacketsTests.swift)**. These tests validate the TLV wire format against the specifications defined in [`bitchat/Protocols/Packets.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/Packets.swift).