Raven — A Steganographic, Metadata-Resistant Messenger
Technical White Paper · v1.1
Scope: Solana mainnet-beta deployment · Last updated 2026-09-07
Status. This is revision v1.1, matching the
v1.1tag in OAraLabs/raven-whitepaper, which is the canonical source. Earlier drafts circulated as "v0.1"; that label is retired and this document supersedes them. The protocol it describes is the one running on Solana mainnet-beta, and the derivations and wire formats are pinned by the published test vectors in OAraLabs/raven-core. Revisions are recorded in the canonical repository'sCHANGELOG.md.
This document describes the mechanism and use case of Raven for a technical audience. It is a layered read: Sections 1–2 and 10 are accessible to any reader; Sections 3–9 are the protocol in full detail; Section 11 is a candid security analysis. It is derived from an internal, sprint-by-sprint engineering specification that is not public; where this paper and the implementation disagree, the implementation is authoritative.
Abstract
End-to-end encryption protects message content, but conventional secure messengers still leak metadata: who is talking to whom, when, how often, and — most fundamentally — that a conversation exists at all. For many users that metadata, and the mere presence of a messaging app, is the threat.
Raven conceals who is communicating with whom — and, from anyone watching the public platform where a carrier is posted, that a conversation is happening at all. Messages are encrypted client-side and stored as opaque blobs on the Solana blockchain; the ordinary public text a sender shares ("carrier text") carries no payload at all. A descriptive encoding rule — a statistical fingerprint of that text — is what tells the intended recipient's client that a message is waiting, and, in stealth mode, where to look. There is no messaging server and no account graph. A shared pool wallet pays all on-chain costs, so a sender never holds cryptocurrency or signs a transaction. Crucially, recipients never poll: a message is discovered only when the recipient deliberately scans text they encounter, or receives a contentless real-time nudge over an optional signaling overlay ("Flock").
What this does not hide is the aggregate. Because every write is signed by the one pool wallet, anyone can enumerate that wallet's transactions and see the complete set of Raven messages ever stored, with timestamps. The volume and timing of all Raven traffic globally are public. What that enumeration does not yield is who any individual message is from, who it is for, or what it says: addresses rotate or are rule-derived, and blobs are opaque. Raven's claim is unlinkability of participants and content, not concealment of the system's own footprint (§2.1, §11).
1. Introduction & Motivation
Modern E2E messengers (Signal, WhatsApp, etc.) solve content confidentiality well. What they do not, and largely cannot, hide:
- The social graph. A server knows that account A exchanges messages with account B. Even sealed-sender designs leak timing and volume.
- Presence and delivery metadata. Online status, read receipts, typing indicators, push tokens.
- The fact of installation. Having the app on a seized device is itself incriminating in some threat models.
Raven takes a different posture, summarized as deniability over convenience. Its carrier is infrastructure an adversary cannot easily take down or correlate: ordinary posts on public platforms (X, Reddit, LinkedIn, …) plus a public, globally-replicated blockchain. A Raven message looks like a normal social-media post to anyone who does not hold the recipient key and the carrier text simultaneously. There is no Raven account to subpoena and no message history on a server to seize: the relayer holds no messages, no keys, and no user records, and the one thing it does persist is a dump-blind link store described in §6.5. It does see the sender's IP at submission time. No record the relayer writes contains that address: rate-limit counters are keyed on a per-boot HMAC of it, and nothing IP-derived reaches durable storage. The only place the address appears at all is a log line emitted when a rate limit is breached, and that line names no recipient. The relayer is rate-limited rather than anonymizing, and the hosting provider maintains its own logs, which are outside Raven's control (§11).
Nor is there background traffic that betrays a user is fetching messages: no client polls Solana on a timer, so a read happens only on a user action or a push the user subscribed to (§2.2). That is not the same as no background traffic at all. On the browser surfaces the optional Flock overlay holds an open WebSocket subscribed to the user's own derived frequency, so a network observer can see that a client is connected to the signaling network and to which frequency — an identifier anyone holding the user's public key can compute (§2.1, §7.3). The iOS client ships with that overlay disabled and makes no such connection (§7.2).
Raven is built as three client surfaces over one shared protocol core, and runs against Solana mainnet-beta.
2. Threat Model & Design Principles
2.1 Adversaries considered
| Adversary | Capability | What Raven denies them |
|---|---|---|
| Passive platform/social-media observer | Reads all public carrier posts; can intercept Flock broadcasts on a public frequency | Steganography hides that any message exists; the carrier is plausibly an ordinary post. The Flock frequency is not among the denials: it is a stable pseudonymous identifier derived from the recipient's public key — the key they hand out to establish contact — so it names no account, but anyone holding that key can compute it and watch it (§7.3) |
| Network observer | Sees relayer and RPC traffic | Content is E2E encrypted before it reaches the relayer; the relayer never sees plaintext; Solana reads are public and unauthenticated, so a read reveals only interest in one opaque address |
| Solana node / chain analyst | Sees every stored blob forever | Blobs are opaque AES-256-GCM ciphertext; addresses are rotating or rule-derived and unlinkable without the counterparty key |
| Pool-wallet enumerator | Enumerates every transaction signed by the single pool wallet, obtaining the complete set of stored ravens with timestamps | Nothing. This is not among the denials. Because one hard-coded signer writes every account (§6.1), the existence, count, timing and growth of all Raven traffic globally are public and trivially monitored, and the pool wallet's address is one query from the published program ID. What the enumeration does not yield is linkage: which ravens belong to the same conversation, who sent or received any of them, or their contents. Per-message unlinkability survives; system-level footprint does not |
| Broadcast slot overwriter | Knows a public broadcast channel's public key, which is in every share and resolvable from any short link, and can write to that channel's slot PDAs | They learn nothing: writing grants no read access beyond what any follower already has. Forgery is denied, because every reader verifies each blob's Ed25519 signature against the share's signing public key and skips what fails. Availability is not among the denials. A write overwrites the targeted slot in place and destroys the raven in it; the other nine are unaffected. The destroyed raven is not recoverable; the owner returns content to the window by broadcasting again (§11) |
| Device seizure | Full access to the unlocked device | Vault is encrypted at rest; the in-memory session view is wiped on lock/close; a compromised key can be socially revoked (see §11 for honest limits) |
2.2 Constitutional commitments
Four principles are treated as non-negotiable invariants in the codebase and the spec. They are quoted here because they define what Raven is:
- No ambient polling. "There is no ambient polling, no periodic background sync, no speculative reads against registered keys… 'Performance' and 'convenience' are not valid reasons to relax this." Every RPC read is the result of a user action or a Flock push for a frequency the user explicitly subscribed to.
- Public keys are not feeds. Broadcast channels expose exactly
PUBLIC_KEY_SLOT_COUNT = 10recent messages in a rolling window — a product decision, not a storage limit. No unbounded history retrieval. - The webapp is a gateway, not a control panel. Administrative and settings surfaces live in the extension/iOS app; the webapp is intentionally limited to keys / compose / scan / tune / about.
- The session view is memory-only. Revealed messages live in memory for the app session in chronological order, then vanish on lock or close. "Nothing is written to disk. Nothing crosses session boundaries."
These commitments remove the timed polling pattern that would otherwise betray a user checking for messages. They are also why several "obvious" convenience features (inboxes, history, background refresh) are deliberately absent.
3. System Architecture
Raven is a monorepo of six packages plus an Anchor smart contract. Three client surfaces share one protocol core; two backend components mediate Solana.
raven-coreis the source of truth for all cross-surface protocol behavior. It is consumed as TypeScript source (no build step) by the extension and webapp, and mirrored byte-for-byte by the SwiftRavenCoreand the Flock SDK.- The relayer's message path is stateless. It receives an already-encrypted blob plus a recipient identifier, signs the on-chain write with the pool wallet, and returns the transaction signature. It never holds plaintext or user keys, and it retains nothing about a relayed message. The service does keep one piece of durable state, unrelated to message relay: a link store for sharing public broadcast keys, described in §6.5.
- The sweeper is a cron job that enumerates aged on-chain accounts and closes them, returning their rent reserve to the pool wallet.
4. Cryptographic Protocol
All cryptography happens client-side. Primitives are standard and conservative.
4.1 Primitive stack
| Layer | Algorithm | Library | Parameters |
|---|---|---|---|
| Key agreement | X25519 ECDH | tweetnacl (scalarMult) |
32-byte keys |
| Key derivation (messages) | HKDF-SHA256 | Web Crypto | salt = empty, info = "raven-v1", → 256-bit key |
| Message encryption | AES-256-GCM | Web Crypto | random 12-byte IV, 16-byte auth tag |
| Broadcast signing | Ed25519 detached | tweetnacl (sign) |
64-byte signature over canonical JSON |
| Vault KDF | Argon2id (legacy PBKDF2) | hash-wasm / Web Crypto | see §8 |
Note on naming. Some internal comments loosely refer to a "
nacl.boxenvelope." In the shipping code, tweetnacl is used only for X25519 scalar multiplication, keypair generation, and Ed25519 signatures. The actual authenticated encryption of every message and the vault is AES-256-GCM via Web Crypto over an HKDF-derived key. This paper describes the implemented primitive.
4.2 Pairwise message encryption
To send to a known recipient, the sender computes the shared secret
X25519(sender_secret, recipient_public), runs it through HKDF-SHA256 with
info = "raven-v1" to obtain a 256-bit AES key, and encrypts the plaintext with
AES-256-GCM under a fresh random 12-byte IV. The wire blob is:
base64( 0x00 ‖ IV(12) ‖ ciphertext ‖ GCM_tag(16) )
The leading flag byte discriminates wire formats. 0x00 = pairwise (the
receiver must already know the sender's identity public key).
4.3 Anonymous first-contact (ephemeral) encryption
For the first message to a freshly imported key, where the receiver does not yet
know the sender, Raven uses an ephemeral variant (flag 0x01). The sender
generates a one-time X25519 keypair, derives the key from
X25519(ephemeral_secret, recipient_public), and places the ephemeral public
key in the clear on the wire:
base64( 0x01 ‖ ephemeral_pub(32) ‖ IV(12) ‖ ciphertext ‖ GCM_tag(16) )
The receiver decrypts with X25519(my_secret, ephemeral_pub): no prior
knowledge of the sender required. The sender's identity public key travels
inside the encrypted body (as sender_public_key) so the two sides can
bootstrap a stable channel afterward (§8.2). This buys sender anonymity for first
contact; it is not a per-message forward-secrecy ratchet (§11).
4.4 Blob versions
A decrypted payload is interpreted by trying versions in order
(decryptRavenBlob: v3 → v2 → v1):
- v1 (legacy): the plaintext is a raw string.
- v2 (private channel): JSON
{ version: 2, message, next_query_id, encoding_rule, timestamp, revoked?, revocation_reason?, sender_public_key? }.next_query_idadvances the rotating-address chain on each successful decrypt (§6.2). Integrity is provided by AES-256-GCM; v2 is unsigned. - v3 (signed broadcast): adds
sender_public_key(Ed25519),signature(Ed25519 detached), and an optional monotonicsequence_number. The signature is computed over the recursively key-sorted canonical JSON of all fields exceptsignature, then the whole blob is encrypted with the §4.2 path. Recursive sorting is a cross-platform interop contract: Swift'sJSONEncoder.outputFormatting = .sortedKeysmust produce identical bytes, or signatures fail to verify across surfaces.
5. Steganographic Encoding
5.1 Carrier text and encoding rules
Raven does not generate cover text. The user writes (or chooses) ordinary public text — the carrier. Raven derives a descriptive fingerprint of that text, the encoding rule, which both (a) selects which sentence in a post carries a message and (b) seeds stealth addressing (§7.4).
An encoding rule is a JSON object with a mandatory vowel backbone
(total_vowels, a_count, e_count, i_count, o_count, u_count) plus a
variable pool of features that are recorded only when present in the carrier:
char_count, word_count, positional word_at[] checks, consonant_count,
longest/shortest word lengths, words starting/ending with a vowel, and counts of
uppercase, digits, punctuation, emoji, and double letters.
5.2 Descriptive, not prescriptive; iterate-present matching
Rules count what is in the text. They never instruct a user to write text a certain way. Matching is iterate-present-only: fields absent from the rule are not checked, and a sentence matches a rule iff every present condition holds. This makes carriers natural-looking while keeping matching deterministic.
5.3 Canonicalization (parity-critical)
Because the same rule must be computed identically on every platform, text
analysis pins exact semantics: NFC normalization, counting by Unicode
codepoint (not UTF-16 units), an enumerated punctuation set (not a
locale-dependent class), and Intl.Segmenter with Extended_Pictographic for
emoji. The canonical byte serialization of a rule (canonicalRuleBytes) is used
directly as HKDF salt in stealth derivation, so its byte-exactness is load-bearing.
6. On-Chain Storage Model
6.1 The program and the pool wallet
The Anchor program (mainnet-beta ID
C9DUi6mFpMxpjkCGw7oxw23Ty63jEg9nujmqgwB4nrWo) stores each message in a Program
Derived Address (PDA). A single hard-coded pool wallet is the only
authorized signer for every store_* and close_* instruction. The relayer
holds that keypair; the sweeper reuses it. Reads are permissionless and free (anyone
can getAccountInfo on a PDA), but they reveal only interest in one opaque
address, not who can decrypt it.
Hard limits, enforced on-chain: MAX_BLOB_LEN = 1024 bytes,
MAX_RECIPIENT_KEY_ID_LEN = 64 chars. Each account holds the 8-byte Anchor
discriminator, its fields, and a blob of up to 1,024 bytes (≈1.05–1.1 KB total).
6.2 Three address families
| Family | PDA seeds | Addressing model |
|---|---|---|
| Private | ["raven", recipient_key_id] |
Rotating per message via a next_query_id chain; recipient_key_id is normalized to ≤32 bytes |
| Public broadcast | ["raven-pk", encryption_pubkey, slot_index] |
Fixed 10-slot rolling window, slot_index ∈ [0, 10) |
| Stealth | ["raven-stealth", recipient_encryption_pubkey, query_id] |
Rule-derived dead-drop, query_id = 16 bytes (§7.4) |
Private addresses rotate: each delivered v2 blob carries the next address in the chain, so an observer cannot link two messages to the same conversation by address. Public channels are intentionally bounded to 10 slots. Stealth addresses are derived from a shared secret and the carrier's encoding rule, so they are unlinkable without both the counterparty key and the exact carrier.
6.3 Economics and lifecycle
The pool wallet funds the rent-exempt reserve when an account is created. The sweeper (cron-triggered, with a kill switch and a dry-run mode) later enumerates program accounts, classifies them by age, and closes the aged ones, returning the reserve to the pool wallet. Default age thresholds:
- Private: closed at ≥ 8 days (1-day grace). Recipients get roughly a
[7d, 8d)"Quiet" window to retrieve. - Public: a channel is closed only when all 10 slots are ≥ 60 days old.
- Stealth: closed at ≥ 90 days (longer fuse, supports late discovery).
This makes write-cost recoverable and bounds the amount of rent-paying account state Raven holds open at any moment.
It does not delete anything. Closing an account removes it from current
state and returns its rent; it does not remove the transaction that created the
account, and that transaction carries the ciphertext in its instruction data.
The blob therefore remains retrievable from ledger history indefinitely, by
anyone, using nothing but the program id and a public RPC. getAccountInfo on
a closed address returns null, while getSignaturesForAddress on the same
address still returns the creating signature, and fetching that transaction
yields the full encrypted blob. Sweeping is an economic mechanism, not a
privacy one: it recovers rent, and it bounds cost, not exposure.
Read the windows in that light. The [7d, 8d) figure and the 60- and 90-day
thresholds are discoverability windows for the recipient's client (how long
the ordinary scan paths can still find and decrypt a raven), not confidentiality
windows. Nothing expires from the adversary's point of view. The correct model
is §2.1's chain analyst, who "sees every stored blob forever."
This compounds with the absence of a forward-secrecy ratchet (§11). Pairwise messages are encrypted under static X25519 identity keys, so a key compromised at any future date exposes every past message it could read — and those ciphertexts are still on chain, addressable, and free to fetch. There is no point at which past traffic becomes unrecoverable, because neither the key nor the ciphertext ages out. Users whose threat model includes later key compromise should treat every raven they have ever sent or received as still exposed.
6.4 The relayer
An Express service exposes:
| Endpoint | Purpose |
|---|---|
POST /submit |
Store a private raven |
POST /submit-public |
Store a public-broadcast slot |
POST /submit-stealth |
Store a stealth raven |
GET /fetch |
Allow-listed (X/Reddit/LinkedIn) URL→plaintext proxy for scanning |
GET /health |
Liveness + pool wallet pubkey |
POST /shortlink |
Mint a short id for a public broadcast key share (§6.5) |
GET /shortlink/:id |
Resolve a short id back to that share (§6.5) |
The relay endpoints validate blob size and identifier shape, then sign and submit; they retain nothing. Two independent layers of rate limiting protect the pool wallet: an always-on in-memory per-IP limiter and a sliding-window per-recipient limiter backed by a shared store. An admin denylist refuses flagged identifiers with a 403 before anything touches the chain, logging no metadata.
6.5 The link store
The two /shortlink endpoints are the one part of the service that persists
data. They exist so a user can hand out a public broadcast key share as a
short link instead of a long opaque string. The store is SQLite on a Railway
volume; if no volume is attached the endpoints answer 503 rather than mint links
into an ephemeral filesystem, and the relay routes are unaffected.
A broadcast share embeds the channel's X25519 secret — the follower read credential — so the store is built on the assumption that its own database will one day be copied. Rows are dump-blind: neither the payloads nor the ids that address them are recoverable from the database alone.
payloads: sha256(shortId) → AES-256-GCM( payload, key = HKDF(shortId) )
dedupe: sha256(payload) → AES-256-GCM( shortId, key = HKDF(payload) )
Short ids exist only inside shared links; payloads only in clients. A link holder
can resolve; a payload holder can dedupe; a disk image shows hashes and
ciphertext and nothing else. A row is (hash, ciphertext). There are no
timestamps, no counters, and no creator identifiers, so the store cannot answer
when a link was made or how often it was used.
Two further properties are deliberate. GET /shortlink/:id performs zero
logging (no access log, no console output, no error detail beyond the status
code) and is registered before the per-IP rate limiter, so the limiter's
abuse-warning path (which records IP and route) does not observe a resolve.
And only public broadcast shares are accepted: private, group, and legacy
1:1 shares are refused with a 400, because pinning a one-to-one share on a
public server would be contact-graph metadata, which is precisely what the rest
of the design exists to avoid.
7. Message Delivery: Two Modes Over One Store
A defining property: both delivery modes write the identical blob to the identical PDA. Flock is a signaling overlay, not a storage path. It never carries ciphertext. The sender chooses per message.
7.1 Manual / stealth-in-the-wild
The sender simply posts the carrier text. The recipient discovers the message by scanning text they come across: no real-time signal, no Flock metadata, maximum deniability. Discovery is always pull, never push.
7.2 Fast / social (Flock)
The sender additionally broadcasts { frequency, carrier_text, timestamp } to
api.theflock.ink. The recipient's auto-tune WebSocket, subscribed to the
recipient's own derived frequency (§7.3), receives a real-time nudge and runs
a scan.
The overlay is not enabled uniformly across surfaces, and the catch-up behavior differs accordingly:
- Browser surfaces (extension, webapp). The WebSocket is live. On reconnect the Flock server replays up to 48 h of history. If the gap since the last Flock-driven activity meets or exceeds that same 48 h buffer, the replay may no longer cover it, so the client fires one Solana catch-up scan on connect. Below 48 h the replay suffices and no extra read is made.
- iOS. The Flock overlay ships disabled. That client does not connect to the signaling network at all: no broadcast, no history replay, and no real-time nudge reaches it. In its place, a single Solana catch-up scan runs on foreground or unlock when at least 1 h has elapsed since the last recorded activity. That scan is the only automatic delivery path on the surface.
The automatic paths do not deliver everything. A discovery floor applies to every bulk scan — history replay, the offline catch-up, and a manual "scan all keys": a private raven younger than 7 days is skipped, and skipped without advancing its address cursor, so it remains retrievable later. Inside that window the raven is discoverable only by scanning the carrier directly, which is the deniable path §7.1 describes and the one the sender chose by posting in the wild. Scans the user initiates against a carrier (paste, screenshot, snip) are never gated, and neither is a live Flock push, which arrives at age ~0. The floor is set against the retention schedule in §6.3 so a raven cannot pass out of the skip window and into the close threshold between two catch-up scans.
None of this is polling: every read is triggered either by an explicit subscription the user opted into or by the user bringing the app to the foreground, and the overlay only ever says "go look."
7.3 Frequency derivation
A frequency is derived from a single public key — the recipient's — so any sender holding that key computes the same channel without coordination. The derivation takes one key, never a pair: a recipient has exactly one frequency, and every sender to that recipient broadcasts on it.
HKDF-SHA256( ikm = recipient_pubkey, salt = zeros(32), info = "flock-frequency-v1", L = 12 )
→ three big-endian 4-byte chunks → (u32 mod 36^4) → base36, zero-padded to 4
→ "@xxxx.xxxx.xxxx"
The 12-byte output yields a short, human-readable handle. This derivation must agree byte-for-byte across TypeScript, Swift, and the Flock SDK; a pinned fixture enforces it.
7.4 Stealth addressing
The stealth path lets a sender drop a message at an address only the intended recipient can compute, derived from the carrier itself:
query_id = HKDF-SHA256(
ikm = X25519(sender_secret, recipient_public), // pairwise shared secret
salt = canonicalRuleBytes(encoding_rule), // raw canonical bytes, NOT hex
info = "raven-stealth-v1",
L = 16 bytes )
The account is a PDA seeded with the recipient's own encryption public key and that tag (not the sender's), so the sender writes into an address in the recipient's key space, and the recipient looks only in their own.
Discovery is a probe, not a lookup. The X25519 agreement is symmetric, so both sides derive the same tag from the same carrier. But a pasted carrier arrives with no sender attribution: the recipient does not know whose key to use. On scanning, the client recomputes the encoding rule from the carrier and then walks its own key list. For each key, one candidate tag is derived and one account read is issued at the corresponding PDA. A miss returns nothing and is silent; a hit is decrypted and surfaced. A scan therefore costs one derivation and one read per key held, and the practical bound on stealth discovery is the size of the user's own key list. There is no directory, no index, and no server-side lookup to consult. Rotating chain addresses are tried first, so an ordinary delivery costs no stealth probes at all.
Stealth blobs sit at a fixed (non-rotating) address and can be re-queried later, enabling late discovery. An observer who sees the carrier post on social media cannot derive the address without the counterparty's key. (For comparison, ordinary private-channel addresses use a different primitive — SHA-256 truncated to 16 bytes — and rotate; the two are intentionally not interchangeable.)
A second variant serves public broadcast channels. A channel has no counterparty, so its tag is derived by a self-agreement — X25519 of the channel keypair with itself — which the channel owner and any follower holding the channel keys both compute identically. That variant seeds the PDA on the channel's public key and carries a v3 signed payload, verified on read, where the pairwise variant above carries v2 and is unsigned (§4.4). A scan probes both families in turn: the recipient's private keys first, then each followed channel.
8. Identity, Vault & Key Lifecycle
8.1 Vault at rest
Each client stores its keys in an encrypted vault. The vault JSON is sealed
with AES-256-GCM under a key derived from the user's passphrase via
Argon2id (memory = 64 MiB, iterations = 3, parallelism = 1, 32-byte output,
16-byte salt). This is CURRENT_KDF_VERSION = 2. Legacy vaults derived with
PBKDF2-SHA256 (100,000 iterations) are version 1 and are transparently
migrated on unlock (re-derived under Argon2id with a fresh salt, re-encrypted,
verified, then written — legacy data is preserved on any failure). The stored
payload tags its kdfVersion, so detection needs no heuristics. The same
StorageAdapter interface backs chrome.storage.local (extension) and IndexedDB
(webapp).
8.2 Identity, keys, and the handshake
An identity is an X25519 encryption keypair plus an Ed25519 signing keypair
(backfilled on first unlock if a legacy vault lacks one). Each correspondent is a
RavenKey record carrying rotating address state
(initialQueryId/currentQueryId/previousQueryId), the last seen encoding
rule, a connection state (pending → imported → connected), a direction, and
revocation status. First contact uses the ephemeral path (§4.3): the first blob
on an imported key embeds the sender's sender_public_key, letting the receiver
bootstrap a stable bidirectional channel.
8.3 Revocation
If a key is compromised, its owner can send a v2 blob with revoked: true. The
counterparty's scanner then stops advancing the chain, marks the key revoked, and
removes it from the compose picker. This is an authenticated social signal,
not cryptographic proof of compromise (see §11).
9. Cross-Platform Parity
Raven's TypeScript core, the Swift RavenCore, and the Flock SDK implement the
protocol byte-for-byte. This is enforced by checked-in JSON fixtures that
pin canonical outputs for: frequency derivation, stealth query_id, channel
derivation, ephemeral encryption, v3 canonical signing, and the vault KDF. Any
change to canonicalization, signing, encoding-rule shape, or a derivation moves
all surfaces and the fixtures together. For a white-paper reader the takeaway is
that Raven's wire and address formats are specified and test-pinned, not
incidental to one implementation.
10. Use Cases
Raven targets users for whom the existence of a conversation is the sensitive fact:
- Close pairs and small circles — couples, families, old friends — who want one channel that is genuinely theirs: no account linking them, no thread to leak or breach, no record that the channel even exists.
- People whose feeds and screens are casually visible to others — shared homes, shared offices, group settings — for whom an ordinary post draws no attention where a secure-messenger notification would.
- Privacy maximalists who want communication with no account graph, no server logs, and no background traffic signature.
Two postures serve these users: fast-social (Flock notifications for active, casual exchange) and maximum-stealth (post in the wild, no real-time signal at all). Because the choice is per message, the same relationship can move between "chatty" and "dark" without changing keys or addresses.
11. Security Analysis & Limitations
Stated candidly, because honest limits are what make the strengths credible.
Strengths.
- Content is end-to-end encrypted with conservative, standard primitives (X25519 / HKDF-SHA256 / AES-256-GCM / Ed25519).
- There is no messaging server and no server-side account graph or contact list.
- On-chain blobs are opaque; private addresses rotate and stealth addresses are rule-derived, frustrating linkage.
- The "no ambient polling" rule removes the traffic-analysis signature that betrays most messengers.
- Broadcast is bounded (10 slots), limiting both feed-like exposure and on-chain footprint.
Limitations.
- No forward secrecy ratchet. Pairwise messages use static X25519 identity keys; the ephemeral path (§4.3) provides first-contact anonymity, not Signal- style per-message forward/future secrecy. Compromise of a long-term key exposes past pairwise messages that an adversary has captured.
- Revocation is social, not cryptographic. Anyone holding a key has equal authority to send or to revoke; revocation is a trust signal, not proof.
- Write authority is centralized. Exactly one signer (the pool wallet, held by the relayer) can write or close accounts. This is what frees senders from holding SOL, but it is a centralization point and a deliberate write-layer censorship vector (the denylist). Reads remain permissionless; only writes are gated. The obvious structural remedy — letting senders sign and fund their own writes — is examined in §11.1; it is not implemented, and it is not straightforwardly better.
- Broadcast slots can be overwritten by anyone who knows the channel. A public broadcast channel's X25519 secret is embedded in every share by design (§6.5), so every follower holds the same read credential the owner holds. Slot addresses derive from the channel public key and a caller-supplied slot index (§6.2), and the program writes in place, creating the account if it is absent and never comparing the incoming write against what is stored. Between the client's send call and the on-chain write there is no signature check, no ownership proof, and no binding between the caller and the channel. Anyone who knows a channel's public key can therefore overwrite that channel's slots, and that key is not a secret: it is in every share, and it is resolvable from any short link. What this does not permit is forgery. Every reader verifies each blob's Ed25519 signature against the signing public key carried in the share (§4.4) and skips anything that fails, so a third-party write cannot place readable content in a channel. Readers decode all ten slots independently, so a hostile write destroys the one raven in the slot it targeted and leaves the rest rendering; the owner returns content to the window by broadcasting again, though the destroyed raven itself is not recoverable. Stated plainly: broadcast availability is not guaranteed. Confidentiality and authorship are.
- The relayer sees sender IP. No record it writes contains the address: rate-limit buckets are keyed on an HMAC of the IP under a secret generated at boot and held only in memory, so a copy of the datastore yields opaque bucket ids, and nothing IP-derived is written to durable storage at all. The only place the address appears is a log line emitted when a rate limit is breached, which records the address and the request path and names no recipient. The relayer is rate-limited but not anonymizing; users who need network-layer anonymity must supply their own (Tor/VPN). It never sees plaintext or keys. Separately, the hosting provider maintains its own logs for the service, outside Raven's control; this paper makes no claim about what they contain or how long they are kept.
- Device compromise is game over for the session. An unlocked device exposes the in-memory session view and, once unlocked, the vault.
- Steganographic strength is scoped. The carrier/encoding-rule scheme resists casual inspection and makes a post plausibly ordinary; it is not claimed to be robust against a dedicated statistical steganalysis of an author's full posting history. Carrier choice matters.
- Unlinkability is structural; the anonymity set is not. Rotating addresses, rule-derived stealth tags, opaque ciphertext and a single uniform signer keep individual messages apart from one another, and they do so at any volume, including a volume of one. What they cannot supply is a crowd. The practical anonymity of a raven is a function of how much other traffic it sits among, and on mainnet today that traffic is small enough to enumerate by hand from the pool wallet (§2.1). At that volume an observer can correlate the handful of writes against external timing, such as a carrier posted publicly shortly before one, without breaking any per-message property. That gap closes with adoption, not with design.
- Mainnet, but early. The deployment runs on Solana mainnet-beta, so rent is paid in real SOL — roughly 0.0084 SOL per message, reclaimed by the sweeper on the retention schedule in §6. Economics at scale (sustained write volume, pool-wallet funding, rent recovery over long horizons) and durability under production load are not yet characterized.
11.1 The self-pay path, and why it is not simply better
Two of the limitations above share a root cause: the relayer sees the sender's IP because the sender asks it to write, and the pool wallet makes all traffic enumerable because one signer writes everything (§2.1). The structural remedy is an optional self-pay path, in which the sender signs and funds their own write. It would remove the operator from the write path entirely. It is not implemented, and it is not straightforwardly better.
It requires a program change, not just client work. The deployed program
hard-codes the pool wallet as the only permitted signer, with a
authority == POOL_WALLET constraint on every store_* and close_* account
struct. The structs are already shaped for it (the payer is the authority),
so the diff is small, but it is a mainnet program upgrade, and it is the same
constraint that gives the denylist and the sweeper their authority.
Four objections, none of which is fatal, all of which are load-bearing:
- The sender's network identity moves rather than disappears. A self-paying client still submits its transaction to some RPC provider, which sees the address and the transaction together. Unless the user runs their own node, that is a third party with a broader view than the relayer and none of its logging discipline. The read and scan paths are untouched either way.
- A self-paid write is attributable; the pool wallet's property is uniformity. Today every raven carries the same signer, so the set is enumerable but no individual write points at a person. A self-paid write is signed by a wallet, and a wallet must be funded from somewhere. Funding trails lead to exchanges and identity documents. That trades "all ravens visible, none attributable" for "each self-paid raven attributable to a funding-traceable wallet," which for most of the users in §10 is the worse half of the trade. A fresh wallet per message would avoid it and few will sustain it; a single-use, freshly-funded wallet is itself a signal.
- Optional means a fragmented anonymity set. In a mixed deployment, self-payers are a small, trivially distinguishable subset of a system whose premise is blending in. The path would be least private exactly when it is newest and least used.
- It bypasses the denylist and both rate limiters. Refusal and throttling live in the relayer. A self-paid write consults neither, leaving the cost of SOL as the only abuse control. The sweeper also cannot reclaim rent it never paid, so self-paid accounts persist until their owner closes them — an economic consequence, not a privacy one, since §6.3 establishes that closure does not remove the ciphertext in any case.
The honest framing is therefore not "self-pay is more private." It is that self-pay removes the operator from the sender's write path at the cost of putting the sender's funding history in it: a good trade for a user who can fund a wallet unlinkably and a poor one for a user who cannot. Offering it as a simple toggle would hand the second user a worse outcome while feeling like a stronger one, which is the failure mode any implementation has to design against.
12. Conclusion & Future Work
Raven demonstrates that hiding that a conversation exists (not only its contents) is achievable by composing well-understood cryptography with a public blockchain dead-drop, steganographic carriers, and a strict no-polling discipline. The system is solo-built and running in production — relayer and sweeper on Railway, webapp on Vercel, iOS and browser-extension clients shipping on their own cadence — today against Solana mainnet-beta.
Natural next directions: a forward-secrecy ratchet for pairwise channels; decentralized or user-run relaying to remove the single-signer trust assumption; economic modeling for rent and sweeping at scale; and a formal steganographic analysis of the carrier scheme.
Appendix A — Parameters & Constants
| Name | Value |
|---|---|
| Program ID (mainnet-beta) | C9DUi6mFpMxpjkCGw7oxw23Ty63jEg9nujmqgwB4nrWo |
MAX_BLOB_LEN |
1024 bytes |
MAX_RECIPIENT_KEY_ID_LEN |
64 chars |
PUBLIC_KEY_SLOT_COUNT |
10 |
| Message KDF | HKDF-SHA256, empty salt, info = "raven-v1" |
| Message AEAD | AES-256-GCM, 12-byte IV, 16-byte tag |
| Wire flags | 0x00 pairwise · 0x01 ephemeral |
| Vault KDF (current) | Argon2id, 64 MiB, t=3, p=1, 32-byte key, 16-byte salt |
| Vault KDF (legacy v1) | PBKDF2-SHA256, 100,000 iters |
Stealth query_id (pairwise) |
HKDF-SHA256, ikm = X25519 pairwise secret, salt = canonical rule bytes, info = "raven-stealth-v1", 16 bytes |
Stealth query_id (broadcast) |
as above, ikm = X25519 of the channel keypair with itself |
Channel query_id |
SHA-256 truncated to 16 bytes (rotating) |
| Flock frequency | HKDF-SHA256, ikm = recipient public key, salt = zeros(32), info = "flock-frequency-v1", 12 bytes → @xxxx.xxxx.xxxx |
| Broadcast signature | Ed25519 detached over recursively key-sorted JSON |
| Sweeper thresholds (default) | private ≥ 8 d (1 d grace) · public ≥ 60 d (all slots) · stealth ≥ 90 d |
| Auto-tune catch-up | browser surfaces ≥ 48 h since last activity (on reconnect) · iOS ≥ 1 h (on foreground/unlock) |
Appendix B — Notation
‖— byte concatenation.X25519(a, B)— Curve25519 scalar multiplication of secretawith publicB.HKDF-SHA256(ikm, salt, info, L)— RFC 5869 extract-and-expand toLbytes.- PDA — Solana Program Derived Address; a deterministic, program-owned account address derived from seed bytes.
- Carrier text — ordinary public text that signals a message is waiting; it carries no payload. Its descriptive fingerprint (the encoding rule) gates recognition and, in stealth mode, address derivation.
- Encoding rule — the descriptive fingerprint of a carrier used for matching and stealth addressing.
The protocol description in this paper reflects the implementation as of 2026-06-24; deployment facts (network and program ID) were updated 2026-07-26 for the mainnet-beta cutover; the delivery, stealth-discovery, and relayer sections were corrected against the implementation on 2026-08-15; and on 2026-09-05 the paper was audited against the code for v1.0: adding the link store (§6.5) and its endpoints (§6.4), adding pool-wallet enumerability to the adversary table (§2.1), reconciling the abstract and §1 against it, and completing the encoding-rule field list (§5.1).