Lortnoc Tahc 01 · Storage
Sui · Walrus · Seal

Every message encrypted, anonymized, and stored immutably.

On a network that can’t read what it’s holding, behind a rule anyone can audit.

Why we built on it

Lortnoc Tahc hides real messages inside platforms that scan everything. Those conversations still have to live somewhere — and if that somewhere is ours, we’ve just made ourselves the easiest thing in the system to subpoena.

So we needed two properties, together: storage that can’t read what it holds, and an access rule we can’t quietly change.

Walrus gives the first — erasure-coded fragments spread across nodes, none of which ever holds a whole blob. Seal gives the second — decryption gated by seal_approve, a Move function running on Sui, rather than a conditional inside our backend.

Neither alone would have been enough. We needed the pair.

Where we currently sit on the trust curve. Seal supports a key-server committee; we run against one server at threshold 1 of 2, so today the honest description is “encryption behind an on-chain access policy” rather than “threshold encryption across a committee”. The policy is real and enforced by a party that is not us — but a single operator could still refuse, or be compelled. Raising the threshold is a configuration change, not a redesign, and it is gated on a client-version conflict rather than on anything we would have to rebuild.

How it works here

  1. Encrypt. Seal encrypts each message to an identity namespaced under its conversation object — headAddress || nonce, so a member of one conversation cannot request shares for another.
  2. Scatter. Walrus erasure-codes it across the network. Every node holds a fragment it can’t read.
  3. Point. A ConversationHead Sui object tracks the current blob. Blobs never change; only the pointer moves.
  4. Find. Conversations announce themselves as ConversationCreated events on Sui, so any device holding the user’s secret rebuilds their inbox from chain with nothing stored by us.
  5. Open. Your Seal session key is signed by a Sui keypair derived on-device from your master secret — so the key servers answer to you, not to us. Reading your own history is always free.
Sending a message
Your messageplaintext, never leaves the page
Seal encrypts itidentity = conversation object address + nonce
Walrus stores iterasure-coded across nodes, written via the upload relay
Sui points at itConversationHead.append(blobId), seq bumped
Reading it back
Read the headthe Sui object lists every blob in order
Fetch the blobreconstructed by a Walrus aggregator
Ask for a key sharethe key server decides — not us
Plaintextassembled client-side
seal_approve(id, head) Before releasing a key share, the key server dry-runs this Move function against live chain state. Two assertions, both required: the identity is namespaced to this conversation object, and the caller is one of its participants.
participant → key share released, message opens
anyone else → NoAccessError, from the key servers

Access follows membership on-chain. We hold no key and cannot override the answer — verified end to end in scripts/seal-live.mjs.

Three properties this depends on

seal_approve is arbitrary Move, not a fixed vocabulary

The usual shape for access control over encrypted data is a menu: allowlist, time-lock, token-gate. Seal makes the policy a Move function evaluated by dry-run, so it can be anything expressible on Sui.

That is what lets our rule be conversation membership rather than an address list we maintain. Add a participant on-chain and they can read; remove them and they cannot, with nothing re-keyed client-side and no list for us to be asked to edit.

Immutable blobs, mutable pointer

A durable append-only log separated from a small object that moves is the right decomposition for a conversation, and it is one we would otherwise have had to invent badly. ConversationHead is that primitive with two extra fields. RedStuff’s self-healing is what makes “your data outlives us” a statement rather than a hope.

Per-blob pricing, which is why we do not batch yet

Walrus bills an encoded floor per blob regardless of size, so every blob under ~217 KB costs the same. Chat is nothing but small objects — a message is a few hundred bytes, which expands about 220,000×. One blob per message is therefore a pricing problem at scale, and Quilt is the answer: at 660 items per blob it is roughly a 650× saving.

We ship one blob per message anyway, and the measurement is why. We wrote a three-message quilt to testnet and it came back at 445,556 bytes — Quilt pads to its sliver structure, so a small quilt carries a large floor of its own and can cost more than the blobs it replaces. The 650× assumes a saturated quilt, and a 1:1 conversation emits messages one at a time. Batching would mean delaying delivery to the recipient to save storage we are not yet spending. The switch is costed and the read path already verified against per-patch aggregator reads, so this is a threshold to cross on volume, not a rewrite.

Specifics

StorageWalrus — erasure-coded, self-healing, immutable
EncryptionSeal, identity namespaced per conversation object (headAddress || nonce)
Access ruleseal_approve, a Move function dry-run by the key servers against live chain state
Key serversOne, at threshold 1 of 2 — an on-chain policy, not yet a committee
BatchingOne blob per message — Quilt measured and deliberately deferred until message volume justifies it
PointerConversationHead Sui object; blobs immutable, only the pointer moves
User keySui keypair derived on-device from the master secret — signs the Seal session key, never transmitted
NetworkSui testnet; Walrus writes via Mysten’s upload relay

PS: There is a business model behind this.

An account is bought once, not funded by reading what you send. The unit economics are public.

See the numbers