ZMSG Protocol v4

The message format ZChat carries in Zcash shielded memos, and over NOSTR

Overview

ZMSG (ZChat Message Protocol) is the wire format for ZChat messages. On the on-chain path a ZMSG line is a pipe-delimited string inside the 512-byte memo field of a Zcash shielded transaction. The same message layer also travels over NOSTR, wrapped as a NIP-17 private DM (NIP-59 gift wrap over NIP-44 encryption), which is how a conversation moves without a transaction per message.

SHIELDED

The ZMSG line rides inside a shielded transaction memo. No server of ours is involved. Each message costs a network fee plus a fixed 1000-zatoshi (0.00001 ZEC) output to a project address, and delivery waits for a block.

TUNNEL

One shielded on-chain KEX anchors the relationship, then messages travel free over NOSTR under the keys the chain anchored.

OPEN

Free and end-to-end encrypted over NOSTR from the first message, with no transaction at all. Unknown senders wait in a Requests inbox.

The transport is chosen per conversation, and a new conversation does not default to the chain. Three ways to carry a message sets out the trade-offs in full.

Message Format

ZMSG|v4|<conv_id>|<type>|<sender_hash>|<payload>
ZMSGProtocol identifier (literal string)
v4Protocol version (v2 and v3 are still parsed for backward compatibility)
conv_id8-character conversation identifier (A-Z, 0-9), uniquely identifies a conversation thread
typeMessage type marker (INIT, KEX, KEXACK, ADDR). Omitted on a plain message, where the sender hash follows the conversation ID directly
sender_hash16 hex characters, first 8 bytes of SHA-256 of sender's Zcash address (12 hex characters in legacy v3 messages, still accepted)
payloadType-specific content

Message Types

INITInitial Message

First message to start a conversation. Includes sender's full address.

<sender_address>|<message_text>
(none)Direct Message

Standard message in an existing conversation. No type marker is sent - the sender hash follows the conversation ID directly.

<message_text>
KEXKey Exchange

E2E key exchange. The ECDSA signature binds the public key to the address the sender reports; it does not by itself prove who the peer is.

KEX:<pubkey_b64>:<sig_b64>
KEXACKKey Exchange Ack

Sent once a KEX signature verifies, carrying the responder's public key.

KEXACK:<pubkey_b64>:<sig_b64>
ADDRAddress Change

Signed notice that the sender has moved to a new unified address.

<new_address>|<signature>
ZREACTReaction

Emoji reaction to a specific message. Sent under its own top-level prefix rather than as a ZMSG type.

ZREACT|<target_txid>|<emoji>|<sender_hash>
ZRCPTRead Receipt

Read receipt for a specific message.

ZRCPT|<target_txid>|<sender_hash>
ZREQPayment Request

Request a ZEC payment from a contact.

ZREQ|<amount_zatoshi>|<sender_hash>|<reason>
ZSTATStatus

User availability status update.

ZSTAT|<status_text>|<sender_hash>
RPLReply (legacy v3)

Quote-reply in the legacy v3 format. Still parsed for backward compatibility, no longer generated.

ZMSG|v3|RPL|<quoted_txid>|<sender_hash>|<reply_text>

Several features ride alongside ZMSG under their own prefixes rather than as ZMSG types: ZTL and ZUNLOCK for time-locked messages, ZFILE for attachments, ZEXP for disappearing-message settings, ZMODE for transport-mode changes, and ZMSG:3.0:GROUP: for group chat, including the signed admin controls that gate kicks and key rotation.

Chunking (v4c)

The 512-byte memo limit applies to the on-chain path. When a shielded message exceeds it, the message is split across several memo outputs inside a single shielded transaction, and the recipient's client reassembles the chunks using the sequence numbers (M/N). It remains one transaction and one network fee, and the platform output is paid once; each extra chunk adds a dust output, so the ZIP-317 fee grows with the number of outputs rather than multiplying per message. Messages carried over NOSTR are not subject to the memo limit and are not chunked.

// First chunk (INIT, then plain)
ZMSG|v4c|1/N|<conv_id>|INIT|<sender_address>|<payload_part>
ZMSG|v4c|1/N|<conv_id>|<sender_hash>|<payload_part>
// Middle chunks
ZMSG|v4c|M/N|CONT|<payload_part>
// Last chunk
ZMSG|v4c|N/N|CONT|<payload_part>

The first chunk carries the full header (conversation ID, type, sender hash) while continuation chunks use the CONT marker to reduce overhead and maximize payload space.

Encryption Layers

1

Transport Encryption (shielded memo or NOSTR gift wrap)

On the shielded path the Zcash pool encrypts the entire memo field at the protocol level, and sender, recipient, amount and memo are hidden from third-party observers of the chain. That is a computational guarantee from Zcash's zero-knowledge proofs rather than an unconditional one, and it does not hide that a transaction happened, so timing and size remain a correlation surface. On-chain messages use the Orchard pool today; ZChat moves to Ironwood once the Zcash Android SDK ships support for it.

On the NOSTR path the same ZMSG payload travels as a NIP-17 private DM, gift wrapped per NIP-59 over NIP-44, published under an ephemeral author key. A relay sees an encrypted envelope, the recipient key it is addressed to, and when it arrived. It does not see the sender or the content. We operate one relay; two third-party relays, relay.damus.io and nos.lol, are also used by default and cannot be turned off in the app today.

2

Application E2E Encryption (negotiated on first contact)

End-to-end encryption using secp256r1 ECDH key agreement, HKDF (RFC 5869) key derivation and AES-256-GCM, over a ZChat ratchet. It applies on every transport, so a Tunnel or Open message is already encrypted by ZChat before a relay ever sees it.

  • Key exchange via the KEX message type. The ECDSA signature binds the public key to the address the sender reports, which authenticates the key to that address rather than proving who the peer is
  • First contact is trust on first use. ZChat shows safety numbers and raises a key-change alert; comparing those numbers out of band is what closes the MITM window
  • Group messages: AES-256-GCM with a shared group key, distributed per member via ECIES and rotated by epoch on membership change
  • No per-message forward secrecy. The ratchet keeps a persistent root, so a retained root re-derives earlier message keys. A full double ratchet is designed and not yet shipped

Encryption flow

Plaintext Message
    |
    v
[AES-256-GCM Encrypt]  <-- ECDH P-256 secret via HKDF, over the ZChat ratchet
    |
    v
ZMSG Payload (encrypted)
    |
    +--> SHIELDED mode
    |        |
    |        v
    |    [Zcash Shielded Memo]  <-- Protocol-level encryption
    |        |
    |        v
    |    Zcash mainnet (opaque to chain observers)
    |
    +--> TUNNEL / OPEN modes  <-- where a new conversation starts
             |
             v
         [NIP-59 gift wrap over NIP-44]
             |
             v
         NOSTR relay (sees an encrypted envelope, the recipient
                      key and the arrival time; not the sender,
                      not the content)

Conversation ID

Format

8 characters, uppercase alphanumeric (A-Z, 0-9)

Entropy

368≈ 2.8 trillion possible values (~41 bits)

Generation

Randomly generated by the conversation initiator

Scope

Shared between both participants of a conversation

Example

A7K3BX9R

Constraints

These are the costs of the on-chain path, and of the shielded handshake that opens a Tunnel conversation. Messages carried over NOSTR pay none of them, and give up the on-chain anchor in exchange.

512-byte memo limitFixed by the Zcash protocol memo field. ZIP 231 memo bundles would raise it and is still in review
Cost per messageA shielded message costs a Zcash network fee plus a fixed 1000-zatoshi (0.00001 ZEC) output to a project address
~75 second block timeShielded delivery waits for a block; a NOSTR-carried message arrives in about a second
No message deletionAn on-chain memo is permanent. A NOSTR-carried message sits on relays instead, and our relay holds relayed events for 30 days
Chunked message costA chunked shielded message is still one transaction; each extra chunk adds a dust output

Source Code

The canonical implementation is in the Android app, MIT licensed and public. It has had no third-party security audit; one has been requested and is pending.

github.com/decentrathai/zchat-android

Protocol parsing

ZMSGProtocol.kt

Encryption

E2EEncryption.kt

NOSTR transport

Nip17.kt, Nip44Encryption.kt, NostrChatBridge.kt

Written specification

docs/ZMSG_PROTOCOL_SPEC.md

Last revised April 2026, so it covers the on-chain ZMSG layer only and predates the NOSTR transport. Updating it is part of the September publication work.

ZMSG Protocol v4 — ZChat Message Format Specification