This directory specifies the Offline Protocol as a wire and behaviour contract, independent of the Rust implementation in this repository. A second implementation written against these documents, in any language, should interoperate with this one.
The Rust crates are the reference implementation, not the definition. Where a document here and the code disagree, that is a bug in one of them, and the document says which reading is normative for the wire.
| Document | Scope |
|---|---|
| Identity and addressing | Address derivation, canonical form, self-certification, session and group identifiers |
| Message model and wire format | The abstract message, the JSON encoding floor, the binary v1 encoding, the extension TLV registry |
| Control messages | The reserved prefix registry, control-plane signing, and the two exemption classes |
| Document replication | Document sync frames, the anti-entropy exchange, attachment references, and what travels the media path |
| Encryption envelopes | The __MLS_ENC__ envelope forms, the media chunk envelope, and the sealed rich payload |
| Group protocol | Group frames, membership commits, leaf identity binding, relay broadcast and the delivery report |
| Capability negotiation | What peers advertise, what each capability gates, and what happens on absence |
| Leaf node provisioning | What a constrained device owes at pairing, the never-committing profile, and the provisioning-time adversary |
| Bluetooth LE framing | The GATT contract, the fragment header, and what a receiver owes on reassembly |
| Username discovery and invites | The self-certifying invite payload, and the non-authoritative username directory |
| The gateway contract | What a gateway is, the five verbs it implements, the gateway-daemon wire protocol, and the backbone |
| Conformance | The two profiles, what every implementation owes, and how the vectors decide it |
Frozen test vectors accompany the chapters that have them, and are the practical definition of interoperating with this implementation. They live in the crate whose code they pin, so a packaged build carries its own vectors:
| Vectors | Chapter |
|---|---|
crates/offline-protocol-core/tests/data/wire-v1.vectors.json |
Message model and wire format |
crates/offline-protocol-core/tests/data/address-v1.vectors.json |
Identity and addressing |
crates/offline-protocol-sealed/tests/data/derive-address-v1.vectors.json |
Identity and addressing |
crates/offline-protocol-sealed/tests/data/control-signing-v1.vectors.json |
Control messages |
crates/offline-protocol-sealed/tests/data/gateway-address-proof-v1.vectors.json |
The gateway contract |
crates/offline-protocol-sealed/tests/data/mls-envelope-v1.vectors.json |
Encryption envelopes |
crates/offline-protocol-sealed/tests/data/key-package-v1.vectors.json |
Capability negotiation |
crates/offline-protocol/tests/data/data-sync-v1.vectors.json |
Document replication |
crates/offline-protocol-transport/tests/data/ble-framing-v1.vectors.json |
Bluetooth LE framing |
crates/offline-protocol-transport/tests/data/nip44.vectors.json |
None. These are the NIP-44 spec's own published vectors, vendored for the Nostr carrier's sealing and pinned to the checksum that spec publishes. Transport framing is out of scope here, so there is no chapter for them to pin |
Every file above was computed independently of the code it pins. A vector computed by running the code under test agrees with any format that code happens to emit, including a wrong one, so it can never report a break.
How that independence is held differs by row, and the difference matters to anyone editing one:
- The first seven rows are generated by
tools/spec-vectors/generate.py, a second implementation of these encodings written from the chapters, which never reads the crates it pins. CI regenerates them and fails on any difference, so an expectation cannot be edited without an edit to the generator beside it. - The document-replication and Bluetooth LE rows were computed by hand from their chapters, before that generator existed, and each states so in its own header. Nothing regenerates them, so their independence rests on review rather than on a job: an edit to one of those files is exactly as load-bearing as an edit to the wire format, and gets read that way.
- The NIP-44 row is the upstream specification's own published file, vendored and pinned to the checksum that specification publishes.
When one fails, the wire format has changed and needs a new version identifier. Editing the expected value to make a test pass converts a caught break into a shipped one. Conformance states the full rule, what the vectors deliberately do not cover, and how a second implementation runs them against itself.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as defined in RFC 2119 and RFC 8174, and appear in capitals only when used in that sense.
The protocol has four independent layers. Each has its own versioning and its own failure mode, and a reader should keep them apart:
- Transport framing. How bytes reach the next hop. Bluetooth LE, Wi-Fi Direct, Reticulum, Nostr, or an internet relay. Out of scope here except where a transport constrains a payload size, and except Bluetooth LE, which has its own chapter. That exception exists because a leaf node's firmware implements the radio itself, so on that one carrier the framing became part of the interop surface rather than an agreement between two builds of this source tree.
- Hop-local encoding. How a
Messagebecomes bytes for one hop. Either the JSON floor or the binary v1 codec. Negotiated per peer viawire_versions, and re-negotiated on every connection. - End-to-end envelope. How ciphertext and its addressing survive an
arbitrary number of relay hops. The
__MLS_ENC__and media envelopes, negotiated viaenv_versions. - Sealed payload. What travels inside the MLS AEAD boundary. Plain text,
or a
__RICH_V1__body, negotiated viarich_versions.
A change at one layer does not imply a change at another. The compact MLS envelope and the binary wire codec ship independently and are gated by separate switches, because a message can take the binary encoding on one hop and JSON on the next while its envelope stays byte-identical end to end.
JSON is the permanent floor. Every conforming receiver MUST accept the JSON
encoding of a Message. A sender MAY emit a compact encoding only to a peer
that advertised it. Persistence and the internet relay path use JSON
unconditionally. No negotiation, capability, or configuration removes the
obligation to decode JSON.
Frozen encodings never change in place. The binary v1 DTO field order and the numeric enum mappings are a wire contract. Additive data goes in the extension TLV section. A change that cannot be expressed additively takes a new version identifier and is negotiated, never assumed.