Raven is our private information retrieval framework for off-chain reads: a wallet fetches state from a server without the server ever learning which record it asked for. This post is about the first deployment, built for the Railgun wallet stack.

Railgun is one of the strongest shielded-transfer protocols on EVM. The on-chain anonymity story is already solved by ZK proofs and commitment Merkle trees. What still leaks is the query side. Before a wallet can build any proof, it has to read state from the chain and from the Proof of Innocence service, and those reads are keyed by the exact commitments the user owns. Whoever serves the reads sees every one of them.

TL;DR

  • Two query-side leaks fingerprint a Railgun user today: full Merkle-tree sync, and POI-list fetching. Both survive the on-chain cryptography intact. The proofs are private. The lookups that feed them are not.
  • Raven sits where the RPC and POI node sit in the wallet's data path. Every query is encrypted before it leaves the client. The operator computes over the dataset without decrypting the request and never learns which entry was fetched.
  • Three wallet calls flip from cleartext to PIR through a drop-in interface implementation. The wallet, the proofs, and the Railgun protocol do not change.
  • Adoption is per wallet and reversible. No contract upgrade, no circuit recompile, no re-audit of the proving path.
  • Live against Ethereum mainnet: demo.railgun.hisoka.io.

Two query-side leaks in a Railgun client

A Railgun client today has two query-side leaks that map directly onto a user's identity. Three wallet calls carry them, and all three are evidenced in the upstream code below.

1. Merkle tree sync

The Railgun contract holds multiple Merkle trees, each fixed at depth 16, so up to 216 (65,536) leaves. A membership proof at depth 16 is exactly 16 sibling hashes, 32 bytes each, a 512-byte path. To produce it the wallet rebuilds and persists every node of every tree locally and then reads the path back out. Fetching only the leaf the user actually needs would tell the provider which leaf the user cares about, so the wallet is forced into the heavy path.

The heavy path is not abstract, and it is far bigger than the tree alone. A filled depth-16 tree is 217-1 = 131,071 node hashes, exactly 4 MiB of raw hash values. But the engine persists much more than the hashes: it stores the full data record of every commitment in the protocol, shield and transact ciphertext alike at roughly a kilobyte each, a second depth-16 transaction-id tree of the same shape, and every nullifier ever emitted, each under a padded key string that itself dwarfs the value. Real local state runs well over a hundred mebibytes per filled tree, and several hundred at mainnet's current trees. None of it is the user's own notes. A wallet that owns three notes still stores the entire protocol history.

It scales with the protocol, not the user, and it only grows: there is no pruning, no eviction, and no light or only-my-notes mode. At the throughput Railgun is targeting, on the order of ten thousand commitments a day, a 65,536-leaf tree fills in under a week, roughly 3.6 million new commitments a year, every one of them stored by every client forever. Railgun already optimizes the cold start with a hosted quick-sync indexer, and it is a real speedup, but it is a bandwidth shortcut, not a storage one: it streams the full ciphertext history from one endpoint, then the client still rebuilds and stores the whole tree locally and still trial-decrypts every commitment with its viewing key. This works for a desktop wallet. It is the structural reason a fully private Railgun client has not reached the browser and the edge devices where most users live.

2. POI list fetching

When the wallet checks Private Proof of Innocence (POI) status it sends its own list of commitments to the POI node, in batches of twenty from one connection. The operator now sees the full set of commitments that belong to one user. Across refreshes the set overlaps, so the cluster is stable even if the IP changes. The list provider already scans every shield and reads the depositor address, so one shielded note in the cluster binds the rest to a real wallet.

It gets sharper at spend time. To build a spend the wallet asks the same node for the Merkle paths of exactly the inputs it is about to spend, before the transaction exists anywhere else, and the request is capped at thirteen, so larger spends split into correlatable calls. This is the most direct fingerprint of a Railgun user in the stack today. The operator only needs to know whether a commitment is innocent, a fact identical for every caller. It never needs the set, and it never needs to know who asked. PIR gives it exactly that and nothing more.

Both leaks survive the on-chain cryptography intact. The proofs are private. The lookups that feed them are not.

How Raven plugs in

The wallet already isolates these calls behind one pluggable boundary: the engine consumes an abstract POINodeInterface that it never constructs itself, and the wallet builds the concrete one internally at engine start. Raven ships a drop-in implementation of that interface, RavenPOINodeInterface. Today the wallet constructs the stock WalletPOINodeInterface from a list of POI URLs, and the interface is not settable through the public API, so making a Raven-backed one active is a small, additive injection point on Railgun's side, the same shape as swapping a transport:

// proposed injection point: accept any POINodeInterface
const poiNodeInterface = new RavenPOINodeInterface({
  endpoint: "https://raven.example.com",
  bearerToken: process.env.RAVEN_BEARER_TOKEN,
});

Three calls flip from cleartext queries to PIR:

Wallet call What it fetches Encoder
getPOIsPerList POI status for a UTXO under each list per-list-status
getPOIMerkleProofs POI authentication path per-list-path
getMerkleProof Commitment-tree authentication path per-leaf-path

Two of those calls, POI status and POI Merkle proofs, flip through the interface above with nothing else touched. The commitment-tree path has no upstream RPC by design, so Raven routes it with one small hook at the wallet's local read site. Both are wallet-layer only; neither reaches the engine, the contracts, or the circuits.

The trust model does not move. Raven transports the path; the wallet verifies the returned siblings against the on-chain Merkle root, the same check it runs today, and still buckets notes and gates spends on the same inputs it always did. A byte-identity test locks every recovered path against the on-chain oracle, so a Raven answer is provably the same bytes the proof would have used. PIR makes the query private, and the answer stays exactly as verifiable as before.

Submission paths (submitPOI, legacy transact proofs) stay as upstream passthroughs. They are public write actions, and the network-layer metadata around them is closed by Nox, the other half of the off-chain privacy stack, so the read side and the network side are both covered. Public-info channels like the commitment-to-index map are cached aggressively; they are the same for every wallet, so there is nothing private to protect.

The wallet doesn't change. The proofs don't change. The Railgun protocol doesn't change. The only thing that changes is that the node serving the lookups can no longer tell which UTXOs belong to whom.

What this means for Railgun

The cost of adopting this is close to zero, because nothing in the protocol moves. No contract upgrade: Proof of Innocence is not enforced on chain. No circuit recompile and no circuit re-audit: the paths and roots the circuits consume are unchanged. No re-audit of the wallet's proving, decryption, or nullifier paths: they are untouched. Adoption is per wallet and reversible. A wallet that points its node URL at a Raven endpoint gets the privacy; one that does not is exactly as it is today; both coexist on the same contracts and circuits indefinitely. There is no flag day.

The payoff lands on distribution. As of Railway Wallet v5.24.4 there are no more mobile application updates, stated by the team to be due to Google Play and Apple App Store policies, and desktop is the supported client going forward. Web apps and browser extensions are the natural next paths, and they are exactly where full-tree sync hurts most. With Raven in front of the POI node and the Merkle-proof endpoint the wallet stops downloading full trees: it asks for the one leaf or commitment it needs, as a single encrypted request in the round-trip budget of an ordinary RPC call, and the operator never learns which entry was fetched. The web app stops syncing hundreds of thousands of leaves on every page load, and the extension becomes practical.

Off-chain query privacy is the floor under any future shielded-protocol UX. For Railgun specifically, it is what makes the protocol shippable in the environments where users actually live.

Try it

A live demo of Raven against Railgun is at demo.railgun.hisoka.io. Source on GitHub.

For the network-layer half of the off-chain privacy stack, hiding who you are when you talk to a chain, see Nox.