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.1 tag 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's CHANGELOG.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:

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:

  1. 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.
  2. Public keys are not feeds. Broadcast channels expose exactly PUBLIC_KEY_SLOT_COUNT = 10 recent messages in a rolling window — a product decision, not a storage limit. No unbounded history retrieval.
  3. 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.
  4. 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.

Backend (never sees plaintext)

Client surfaces (hold keys, do all crypto)

encrypted blob + key id

signs store_* tx

close_* aged PDAs

free getAccountInfo read

optional broadcast / subscribe

Chrome MV3 extension
popup · service worker · content scripts · OCR

Webapp · sendraven.ink
(Vite SPA, Vercel)

iOS app
(SwiftUI + CryptoKit)

raven-core (TypeScript)
crypto · encoding · keys · vault · scanning · frequency

RavenCore (Swift)
byte-for-byte mirror

Relayer (Express, Railway)
holds pool-wallet keypair

Sweeper (cron, Railway)
reclaims rent

Anchor program (Solana mainnet-beta)
C9DUi6…B4nrWo · pool wallet = only signer

Flock signaling overlay
api.theflock.ink (no ciphertext)


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.box envelope." 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):


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:

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.

Recipient (client)Flock (optional)Solana PDARelayer (pool wallet)Sender (client)Recipient (client)Flock (optional)Solana PDARelayer (pool wallet)Sender (client)alt[Fast / social][Stealth / in the wild]derive encoding rule from carrier textencrypt blob (X25519 → HKDF → AES-256-GCM)POST /submit (blob + key id)store_* (signed by pool wallet)broadcast {frequency, carrier_text}real-time auto-tune pushgetAccountInfo + decryptpost carrier text publiclyencounter & scan carrier textgetAccountInfo + decrypt

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:

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:

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.

Limitations.

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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


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).

Canonical source: github.com/OAraLabs/raven-whitepaper
Revision 3f93858 · page built 2026-09-30
This page is generated from the Markdown in that repository. If the two disagree, the repository is correct.
© 2026 OAra Labs, LLC · CC BY 4.0