live · mainnetoc · docs
specs · api · guides
docs / specification

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 kind values and new optional blocks (postage, seal sub-fields) are additive; clients MUST preserve unknown fields when relaying and MAY ignore them when decrypting. Incompatible changes increment OC Lock's envelope.v (currently 2).

What OC Chat adds (§1)

OC Lock operating continuously over a thread, plus three things the base protocol does not have:

  1. A recipient-independent content id so a held envelope can be re-keyed after the fact (§3).
  2. Three canonical send modesspeak-now, pay-to-reach, seal-til-block (§4) — each adding exactly one Bitcoin-unique property.
  3. 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)

kindModeNew fields
"chat"speak-now / pay-to-reachoptional postage (§6)
"chat-seal"seal-til-blockseal (§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 publicrecipients[] 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" + a postage block. A stranger's first message must carry a valid Lightning preimage paid direct to the recipient.
  • seal-til-block (§4.3) — kind="chat-seal" + a seal block. The key is wrapped to a named beacon, released only after the chain passes unlock_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:

CodeMeaning
E_BLOCK_UNMETSeal release requested before unlock_block + confirmations confirmed.
E_BEACON_UNAVAILABLESeal beacon did not respond or could not be reached.
E_NO_POSTAGEA pay-to-reach envelope from a non-contact lacked valid postage for the floor.
E_BAD_POSTAGESHA-256(preimage) != payment_hash, or the nonce/recipient/amount binding did not match.
E_THREAD_GAPparent_id does not resolve to a held parent envelope id.
E_QUEUE_ROUTEA durable-inbox deposit/drain referenced a queue_id the caller is not entitled to, or a malformed queue id.
E_DIR_UNVERIFIEDA kind-30114 listing failed the §8.2.2 gate (signature / kind-30078 binding / UTXO floor).
E_DIR_REVOKEDThe resolved listing is a tombstone or absent — the handle is not discoverable.
E_CHANNEL_RECIPIENTSA chat-channel envelope carried a non-empty recipients[].
E_CH_POLICY_INVALIDA descriptor's write.rooted flag did not match its write.policy.
E_CH_UNAUTHORIZEDA descriptor replacement or moderation tombstone was signed by an out-of-roster address.
E_CH_WRITE_DENIEDA channel post failed the channel's write.policy gate.
E_CH_NOT_WRITERThe post author holds a reader-only role on the channel.
E_CHAN_FLOORA utxo-floor post's write_proof failed (bad control_sig, below value, or under age).
E_RELAY_AUTH_REQUIREDAn auth-required relay rejected a publish/subscribe pending NIP-42 AUTH.
E_FEDIMINT_UNAVAILABLEA named-Fedimint postage endpoint did not respond.
E_FEDIMINT_BINDING_MISMATCHA 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).

KindObjectd-tag prefixStatus
30110channel descriptor (addressable)oc-lock-chat-ch:v1
30111channel post (chat-channel, addressable)oc-lock-chat-msg:v1
30112seal / block-height anchor descriptoroc-lock-chat-seal:v0
30113group-key rotation (private channels)oc-lock-chat-*:reserved
30114discoverability directory (addressable)oc-lock-chat-dir:v1
30115postage-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_aad and chat_envelope_id with recipients=[] for kind ∈ {chat, chat-seal} and reproduces the test vectors.
  • Carries conversation_id/seq/parent_id inside the encrypted payload and orders by the parent_id hash-chain, never by created_at.
  • If it offers durable store-and-forward: derives queue_id/bootstrap_id per §8.1, routes solely on opaque ids, stores the blob byte-for-byte, reproduces vc06.
  • 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: carries postage in 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 for seal-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 + matching rooted flag, validates the governance hash-chain on every replacement, rejects a non-empty recipients[] channel post, verifies utxo-floor proofs offline, renders non-rooted channels in the muted "via ochk.io" tier, honors tombstones, and reproduces vc14vc17.
  • If it supports an auth-required relay: 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