Specification
OC Chat is a mode of OC Lock, not a new verb. This page is the normative reference, condensed and cross-linked to the concept pages. The base cryptography — X25519 ECDH + HKDF-SHA256 + AES-256-GCM, RFC 8785 canonicalization, BIP-322 identity binding — is unchanged from OC Lock and not restated here. Read the OC Lock spec first.
The full normative document lives in
oc-chat-protocol/SPEC.md;
this page mirrors its structure. The key words MUST, MUST NOT, SHOULD, MAY are
per RFC 2119.
Status: v0, draft. New
kindvalues and new optional blocks (postage,sealsub-fields) are additive; clients MUST preserve unknown fields when relaying and MAY ignore them when decrypting. Incompatible changes increment OC Lock'senvelope.v(currently 2).
What OC Chat adds (§1)
OC Lock operating continuously over a thread, plus three things the base protocol does not have:
- A recipient-independent content id so a held envelope can be re-keyed after the fact (§3).
- Three canonical send modes —
speak-now,pay-to-reach,seal-til-block(§4) — each adding exactly one Bitcoin-unique property. - Encrypted threading with a verifiable hash-chain (§5).
Everything else (group keys, double-ratchet forward secrecy, the CLTV-witness seal) is a registry extension under reserved kinds (§10), not a canonical surface.
Envelope kinds (§2)
kind | Mode | New fields |
|---|---|---|
"chat" | speak-now / pay-to-reach | optional postage (§6) |
"chat-seal" | seal-til-block | seal (§7) |
"chat-channel" | public channel post (§8.3) | channel_id, write_proof |
A conforming OC Lock implementation that does not understand these kinds MUST
reject them rather than mis-decrypt. A chat-channel post is public —
recipients[] MUST be empty (E_CHANNEL_RECIPIENTS otherwise). Detail:
Envelope.
Content addressing — the recipient-exclusion rule (§3)
For kind ∈ { "chat", "chat-seal" }, recipients[] is delivery routing, not
content, and is excluded from both the id and the AAD:
chat_aad = SHA-256( canonical( env | id="", ciphertext="", sig.value="", recipients=[] ) )
chat_envelope_id = SHA-256( canonical( env | id="", sig.value="", recipients=[] ) )
A holder MAY replace recipients[] (a re-wrap) and the id, sig, and
ciphertext tag remain valid — the re-wrap output is a detached recipients[]
entry merged locally, never a recompute of the signed id. Proven by vector
vc04. Full rule:
Envelope & content addressing.
The three send modes (§4)
speak-now(§4.1) —kind="chat", no postage/seal. Per-device X25519 authenticates the send; BIP-322 is not signed per message. Free-tier anti-spam is recipient policy (mutual contact OR a BIP-322 UTXO floor). A non-clearing first message is delivered to a filtered/pending state, never dropped; acceptance is a private, revocable allowlist.pay-to-reach(§4.2) —kind="chat"+ apostageblock. A stranger's first message must carry a valid Lightning preimage paid direct to the recipient.seal-til-block(§4.3) —kind="chat-seal"+ asealblock. The key is wrapped to a named beacon, released only after the chain passesunlock_block. Beacon-enforced, not consensus — "trustless" is forbidden.
Detail: The three send modes.
Threading (§5)
Thread state is inside the encrypted payload:
{body, conversation_id, seq, parent_id, recv_queue?}. parent_id MUST equal
the chat_envelope_id of the parent (or null); clients order by the
hash-chain and MUST NOT trust created_at. Attachments (§5.1) ride E2EE inside
the payload (inline v0 profile, ~100 KiB; blob-store upgrade for larger).
Detail: Threading & attachments.
Postage (§6)
A recipient publishes a floor_sats + an identity-signed Lightning endpoint
(LNURL + LUD-18 in v0; BOLT12 recommended). The sender's postage block rides
inside the encrypted payload (§6.2). The recipient runs the six-step offline
verification (§6.3): settlement, the re-derived description-hash binding,
recipient, amount, nonce, and a local spent-ledger. No OC payment rail
(§6.4); zap receipts are forbidden as proof. A named-Fedimint custodial fallback
(§6.5) is gated on a money-transmitter analysis. Detail:
Postage.
Seal (§7)
The seal block carries unlock_block, anchor (beacon|cltv),
beacon_id, beacon_url, redundant_beacon, confirmations. v0 ships the
in-ciphertext drand-tlock profile (§7.6): the body is locked under a reveal
secret tlock'd to a drand round, with a hard chain gate (the recipient MUST
independently confirm chain_tip ≥ unlock_block + confirmations before
surfacing the body). The beacon-device re-wrap path (§7.2–7.3) is the named
upgrade target. Standing delivery (§7.7) is a max-wins composition. The
anchor="cltv" consensus path (§7.4) is reserved. Detail:
Seal-til-block.
Transport (§8)
The envelope travels as the inner blob of a Nostr kind-1059 gift-wrap signed
by an ephemeral key. The v2 encrypted wrap (§8, ct="oc-chat/v2") encrypts
the content to the recipient inbox key, closing the social-graph leak. A
durable inbox (§8.1) uses opaque per-conversation queue_ids
(HMAC(HKDF(device_sk), conversation_id)) + a derivable bootstrap queue.
NIP-42 relay AUTH (§8.4, kind-22242) is the institutional metadata
hardening. Detail: Transport & durable inbox.
Directory (§8.2)
A kind-30114 opt-in, addressable listing keyed by a salted handle hash. A
resolver honors a handle only if all three hold: signed by inbox_pubkey;
inbox_pubkey device-bound (kind-30078) to address; and address clears the
UTXO floor (funded + aged). The social-graph firewall (§8.2.3) keeps any
edge metadata out. Revocation is by tombstone, forward-effective only. Detail:
Discoverability directory.
Channels (§8.3)
A kind-30110 founder-rooted governance descriptor + kind-30111 posts + a
Bitcoin-priced write gate. v1 ships public channels. Exactly one
write.policy with a matching rooted flag; only utxo-floor is
Bitcoin-load-bearing. Five roles enforced offline by signature; moderation by
forward-effective tombstone. Private channels (§8.3.7) are reserved behind the
same descriptor. Source-intake (§8.5) is a composition. Detail:
Channels.
Errors (§9)
In addition to OC Lock §6 codes:
| Code | Meaning |
|---|---|
E_BLOCK_UNMET | Seal release requested before unlock_block + confirmations confirmed. |
E_BEACON_UNAVAILABLE | Seal beacon did not respond or could not be reached. |
E_NO_POSTAGE | A pay-to-reach envelope from a non-contact lacked valid postage for the floor. |
E_BAD_POSTAGE | SHA-256(preimage) != payment_hash, or the nonce/recipient/amount binding did not match. |
E_THREAD_GAP | parent_id does not resolve to a held parent envelope id. |
E_QUEUE_ROUTE | A durable-inbox deposit/drain referenced a queue_id the caller is not entitled to, or a malformed queue id. |
E_DIR_UNVERIFIED | A kind-30114 listing failed the §8.2.2 gate (signature / kind-30078 binding / UTXO floor). |
E_DIR_REVOKED | The resolved listing is a tombstone or absent — the handle is not discoverable. |
E_CHANNEL_RECIPIENTS | A chat-channel envelope carried a non-empty recipients[]. |
E_CH_POLICY_INVALID | A descriptor's write.rooted flag did not match its write.policy. |
E_CH_UNAUTHORIZED | A descriptor replacement or moderation tombstone was signed by an out-of-roster address. |
E_CH_WRITE_DENIED | A channel post failed the channel's write.policy gate. |
E_CH_NOT_WRITER | The post author holds a reader-only role on the channel. |
E_CHAN_FLOOR | A utxo-floor post's write_proof failed (bad control_sig, below value, or under age). |
E_RELAY_AUTH_REQUIRED | An auth-required relay rejected a publish/subscribe pending NIP-42 AUTH. |
E_FEDIMINT_UNAVAILABLE | A named-Fedimint postage endpoint did not respond. |
E_FEDIMINT_BINDING_MISMATCH | A federation-fronted postage invoice failed the §6.3 binding. |
Nostr kind registry (§10)
OC Chat claims 30110–30115. d-tags are verb-rooted (oc-lock-chat-*)
because chat is a mode of the lock verb. The transport gift-wrap stays kind-1059
(NIP-59); device records stay kind-30078; relay AUTH uses kind-22242 (NIP-42,
ephemeral, never stored).
| Kind | Object | d-tag prefix | Status |
|---|---|---|---|
| 30110 | channel descriptor (addressable) | oc-lock-chat-ch: | v1 |
| 30111 | channel post (chat-channel, addressable) | oc-lock-chat-msg: | v1 |
| 30112 | seal / block-height anchor descriptor | oc-lock-chat-seal: | v0 |
| 30113 | group-key rotation (private channels) | oc-lock-chat-*: | reserved |
| 30114 | discoverability directory (addressable) | oc-lock-chat-dir: | v1 |
| 30115 | postage-policy record (pay-to-post) | oc-lock-chat-*: | reserved |
The authoritative cross-family table is the workspace
KINDS.md and oc-agent-protocol/SPEC.md §4.
Compliance checklist (§11)
A client is OC Chat v0 compliant iff it:
- Reuses OC Lock §4.2 wrapping verbatim for
content_key. - Computes
chat_aadandchat_envelope_idwithrecipients=[]forkind ∈ {chat, chat-seal}and reproduces the test vectors. - Carries
conversation_id/seq/parent_idinside the encrypted payload and orders by theparent_idhash-chain, never bycreated_at. - If it offers durable store-and-forward: derives
queue_id/bootstrap_idper §8.1, routes solely on opaque ids, stores the blob byte-for-byte, reproducesvc06. - If it offers the directory: publishes only on explicit opt-in (default
invisible), derives the salted-handle d-tag (
vc07), refuses to resolve unless the §8.2.2 gate passes, enforces the social-graph firewall, honors tombstones (vc08), and exposes no bulk-dump endpoint. - For
pay-to-reach: carriespostagein the payload and runs the full §6.3 verification (re-deriving the description-hash binding itself, over verbatim bytes), routes a failing/replayed message to filtered/pending (never dropped), and never claims transferable third-party proof. - Wraps to the beacon and performs release re-wrap as a detached
recipients[]merge forseal-til-block, and never labels a v0 beacon seal "trustless." - Surfaces every trust anchor (beacon id/url, relay, redundant beacon) and the early-release / brick risks at compose time.
- Operates no OC payment rail for postage.
- If it offers public channels: publishes a founder-rooted descriptor with one
write.policy+ matchingrootedflag, validates the governance hash-chain on every replacement, rejects a non-emptyrecipients[]channel post, verifiesutxo-floorproofs offline, renders non-rooted channels in the muted "via ochk.io" tier, honors tombstones, and reproducesvc14–vc17. - If it supports an
auth-requiredrelay: completes the kind-22242 handshake, signs with a fresh per-connection ephemeral key by default, surfaces the relay as a named trust anchor, and never presents AUTH as making a relay trustless. - If it offers source-intake: warns the source the post is public + permanent, defaults to a fresh throwaway identity, never claims SecureDrop-grade anonymity.
- If it offers a named-Fedimint postage fallback: surfaces the federation as a named custodian, lets the sender decline, keeps OC out of custody, and gates on a money-transmitter analysis.
- Emits the §9 + OC Lock §6 error codes.
Source documents
SPEC.md— the full normative text.PROTOCOL.md→ mirrored at Protocol walkthrough.WHY.md→ mirrored at Why OC Chat.SECURITY.md→ mirrored at Security posture.test-vectors/— five+ reproducible fixtures, includingvc04(re-wrapid/tag stability) andvc05(preimage verification).