voidcashdocs

The indexer

What the indexer stores and serves, how it checks itself against the chain, the alerts it sends on large exits, pauses, parameter changes and upgrades, and its API.

To find its notes, a vault has to try every ciphertext in the pool, and to know which are spent it needs every nullifier. Reading all of that from Solana on every unlock would be slow, so the indexer follows the pool program and serves it in pages. It is a convenience: everything it serves comes from the chain and is checked against the chain, and it can be rebuilt from the chain at any time.

What it stores

The indexer reads the program's transactions from JSON-RPC at confirmed, oldest first after its cursor, and skips failed ones. From each transact it takes:

DataFrom
Leaves: index and commitmentThe program's Inserted events
CiphertextsThe instruction data, two per transact
NullifiersThe instruction data, two per transact
SOL that left the vaultThe Exited events
Activity: the kind and, for shields and unshields, the amountThe sign of ext_amount: shield, private or unshield. A private transfer and a merge look the same and carry no amount
Block timeThe RPC

A Program data: log line counts only while the pool program itself is executing, so events emitted by any other program are ignored. The commitments in each event must equal the ones in the instruction, or the indexer stops on that transaction.

Storage is Postgres: leaves, nullifiers, activity, the indexing cursor and the health status. Each Solana transaction is applied in one SQL transaction, and its leaves must continue the tree exactly.

Every instance serves HTTP from Postgres. The instance that holds a Postgres advisory lock also indexes, polling every 2 seconds; the others keep trying for the lock, so one takes over if the writer stops.

Self-check

The writer keeps the tree in memory. When it has as many leaves as the program, its root must be in the program's root history. If it is not, the writer alerts and marks the indexer unhealthy, and every instance answers 503 instead of serving leaves, nullifiers or activity.

Clients do not rely on that. Before every proof the app makes the same check itself, against the tree account it reads from the chain:

  • an indexer that drops, changes or invents a leaf gives a root the program does not hold, and the app proves nothing against it;
  • a nullifier the indexer leaves out makes a spent note look unspent; spending it fails at the program, because the nullifier's account already exists.

The indexer serves the same data to every client, and clients download all of it, so their requests do not say which notes are theirs.

Alerts

Every alert goes to the log, and to Telegram and a webhook when they are configured. Exits are public on-chain, so the messages carry no private data.

AlertWhenDefault threshold
Large exitOne Exited amount above a share of the vault's balance before it200 bps (2%)
Exits in a windowExits within a window of slots summing above a share of the vault (the balance after them plus their sum). The window starts over after it fires500 bps (5%) over 9,000 slots (about an hour)
PauseAny change of the config's pause flags
ParametersAny change of the fees, the buyback share or the minimum deposit
UpgradeThe program's ProgramData shows a new deployment slot
MismatchThe indexer's tree does not match the chain; serving stops

API

PathResult
GET /v1/status{ healthy, leaves, nullifiers, root, slot }. Served even when unhealthy
GET /v1/leaves?from=&limit=[{ index, commitment, ciphertext }], up to 5,000 per page. Commitments as decimal strings, ciphertexts as hex
GET /v1/nullifiers?from=&limit=Decimal strings, up to 5,000 per page
GET /v1/activity?limit=The latest transactions, newest first, up to 100: [{ signature, slot, time, kind, lamports, commitments }]. lamports is null for private transfers
GET /v1/stats{ leaves, nullifiers, transactions, day: { counts, shielded, unshielded, hourly } }: counts per kind all time and over the last 24 hours, the day's volumes in lamports, and 24 hourly counts, oldest first

The app reads leaves and nullifiers page by page, and refuses leaves that arrive out of order. The site's live numbers come from /v1/stats and /v1/activity.

On this page