DEV Community

ztext_zksnarks
ztext_zksnarks

Posted on • Originally published at z-text.org

How We Fit a Messenger, Wallet, and Password Manager Into 482 Bytes

A BitcoinZ shielded transaction gives you exactly 512 bytes of memo space. No more, no less — it's a consensus rule, not a setting anyone can change.

After encryption overhead, you're left with 482 usable bytes.

That's the entire budget we had to fit a private messenger, a password manager, a BTCZ wallet, and broadcast channels into — for Z-TEXT, a zk-SNARKs blockchain messenger built on BitcoinZ. No server holds any of it. Here's what that constraint actually forced us to build.

1. Why 512 bytes, and why we can't change it

Z-TEXT doesn't own or control the BitcoinZ blockchain. BitcoinZ is an independent chain, live since 2017, and we build on top of it the same way any wallet builds on top of Bitcoin.

We could have built our own chain with any memo size we wanted. We didn't — we chose to build on an existing, independent chain, and that meant accepting its limits as-is.

Before any content is written, the encryption envelope takes its cut:

  • 1 byte — version
  • 12 bytes — encryption IV
  • 16 bytes — authentication tag
  • 1 byte — compression marker

30 bytes gone before a single character of content. What's left — 482 bytes — is the budget for every message, password entry, wallet transaction, and channel post.

2. The message pipeline

Every message goes through the same seven steps:

  1. Packed — MessagePack, a compact binary format, with integer field keys instead of named ones
  2. Signed — Ed25519, covering every field except the signature itself
  3. Optionally compressed — only if it actually helps (more on this below)
  4. Encrypted — AES-256-GCM, key derived only by the recipient
  5. Framed — IV + ciphertext + auth tag
  6. Versioned — one byte prepended, so the format can evolve
  7. Sent — one 512-byte memo, one BitcoinZ shielded transaction

One deliberate ordering choice: we sign before encrypting, not after. Sign-then-encrypt means the signature is protected by the ciphertext. Encrypt-then-sign would let anyone holding the group's decryption key strip off the real sender's signature and forge their own — an open door we didn't want.

3. Making 482 bytes go further

Every one of these was measured, not assumed:

Technique Replaces Measured effect
MessagePack JSON ~40% smaller
Integer field keys Named keys ~15% smaller again
Sender address dropped Address in payload Free — recovered from the tx itself
Binary values kept binary base64 text base64 costs 1/3 more; 413B → 552B
Topic lists as positional arrays Named-key maps ~half the cost per topic
128-bit tx references Full tx IDs Half the length, still unambiguous
Media as a reference Inline media data A GIF becomes a few characters

These aren't just optimizations — they're feature gates. Our Topics feature exists because positional arrays roughly halve the cost per topic. With named JSON keys, the topic list wouldn't fit in the memo at all, and the feature wouldn't exist.

4. Three bytes over the limit

This wasn't a hypothetical — it happened.

On August 3, we decoded a real community invite off a phone. 15 fields: viewing key, signature, address, epoch key, owner's public key, avatar, name, salt, timestamps. Payload: 430 bytes. Wrapped in the encryption envelope: 515 bytes.

The limit is 512. Three bytes over — roughly the width of the word "the."

The fix: strip the avatar and description from the invite (both arrive seconds later via the owner's identity broadcast anyway), re-sign. Result: 411 bytes, a 496-byte memo, 16 bytes of headroom.

What's worth flagging is what happened before the fix. An earlier version of the size-check code guessed the invite was 413 bytes and assumed it fit. The guess was 17 bytes light. The feature silently failed to send, the first time anyone used it.

There's now a test that measures the real byte count on every build. A guess can't make that call again.

5. Compression can make things bigger

Gzip-style compression costs roughly 23 bytes of overhead for headers and trailers. A channel invite is mostly a signature and random key material — data that's already high-entropy and effectively incompressible.

Compress that, and you don't save bytes. You add 23 bytes of overhead on top of data that didn't shrink. On the exact payload closest to the limit, that was enough to blow the budget and break the entire invite-sharing feature.

The fix: never assume. For every message, we check both compressed and uncompressed size, and keep whichever is smaller — recorded in a single marker byte.

6. When one memo genuinely isn't enough

Some things are just bigger than 512 bytes. A post-quantum public key is 1,184 bytes — more than double the budget. Those get split across multiple memos and reassembled.

The interesting design decision is in the failure case. Only the last chunk carries a signature, and that signature covers a hash of every chunk in order. A half-arrived key can never be accepted — not because we detect and reject it, but because it's cryptographically impossible for an incomplete key to produce a valid signature. The failure mode we're closing isn't "a chunk arrives late" — it's "a partial key gets trusted as real."

7. No server holds the moderation state

This is the part that took the most engineering: building Discord-style moderation — roles, bans, suspensions, pinned posts — with no server anywhere holding the authoritative state.

Every action (promote, ban, pin) is a signed instruction written into the same 512-byte memo space. Every client independently replays the full history of these instructions and arrives at the same state. Three things make that consistent:

  • A total order everyone agrees on — sorted by block height, then tx ID, then position within the tx. No clocks, no coordination.
  • Signed per-sender sequence numbers — a sender's own operations can't be reordered by a hostile relay.
  • A deferral fixpoint — if a device sees "user X promoted" before it sees "user X joined," it holds the operation and retries, rather than guessing or dropping it.

A community's identity is derived, not assigned — a hash of the owner's key, address, and a salt, recomputed by every joiner. And enforcement lives on the receiving side: a modified client can hide every restriction in its own UI, but it still can't make an unauthorized action stick, because every other honest client independently rejects it.

That's the property worth naming: Z-TEXT is censorship-resistant by construction, not by policy or promise.

8. Why this is hard to copy

A messaging UI can be rebuilt in a month. This can't — a competitor would need to redo the byte budget, the replay engine, the ordering rules, the deferral logic, and a backward-compatible wire format, before shipping a single user-visible feature.

One detail shows why it compounds: when we added Topics to Communities, the topic ID field is only written when it's non-zero. A post with no topic is byte-for-byte identical to a pre-Topics post. Every old signature still verifies — a protocol upgrade that cost existing users nothing.

It's also why we can now ship new capability without touching server infrastructure — every new feature is just a new signed operation running through the same replay engine. Channels, Awards, and Communities each took weeks, not quarters, because of this.

One open question we're actively exploring: AI agents will likely need the same thing people do — a way to act and prove it happened, without exposing the private data behind it. Whether this same architecture ends up serving that need is still open.


Full writeup with an infographic and video breakdown: https://z-text.org/482-bytes-of-freedom-2026/

Happy to go deeper into the replay engine, the Rust implementation, or the note-splitting mechanics in the comments if there's interest.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.