Running an Indexer
The ZNS indexer can be run to independently verify registry state - this is what makes ZNS trust-minimized.
An indexer you run yourself will derive the exact same name-registry database as everyone else watching the same chain with the same UIVK and admin pubkey.
Build
git clone https://github.com/zcashme/ZNS.git
cd ZNS
cargo build --release --features testnet # or --features mainnetThe testnet and mainnet features are mutually exclusive. Exactly one must be enabled at compile time - they pick different lightwalletd URLs, birthday heights, and path defaults.
Run
export ZNS_UIVK="uivktest1qqq…"
export ZNS_ADMIN_PUBKEY="0123abcd…64 hex chars…"
cargo run --release --features testnetThe RPC server binds to 127.0.0.1:3000 by default. To expose it externally, front it with a reverse proxy (nginx, caddy) - the indexer has no built-in auth, TLS, or rate limiting.
Configuration
All configuration goes through two channels:
Compile-time constants (in src/config.rs):
- Network (
testnetormainnetCargo feature) - lightwalletd URL
- Birthday height
- SQLite database path
- RPC listen address + port
Runtime environment variables:
| Variable | Required | Description |
|---|---|---|
ZNS_UIVK | yes | Unified Incoming Viewing Key for the registry wallet |
ZNS_ADMIN_PUBKEY | yes | 32-byte Ed25519 admin public key, hex-encoded |
Missing either secret causes the indexer to exit immediately on startup.
First sync
On a fresh database, the indexer scans from the birthday height forward. Expect:
- No-op blocks until the first
SETPRICEmemo is observed on chain. CLAIMs before pricing exists are rejected. - Bootstrap SETPRICE - this establishes the initial pricing tiers and unblocks claims.
- Backfill - every CLAIM / UPDATE / LIST / … up to the chain tip is processed in order.
Sync time depends on chain length and lightwalletd responsiveness. A testnet cold sync is a few minutes; mainnet is longer.
Verify state
Once the indexer has caught up, query it:
curl -sX POST http://localhost:3000 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"status","params":{}}' | jq{
"result": {
"synced_height": 3902500,
"admin_pubkey": "abc123…",
"uivk": "uivktest1qqq…",
"registered": 42,
"listed": 3,
"pricing": { "nonce": 1, "height": 3901000, "tiers": [500000000, 100000000, …] }
}
}Compare registered, listed, and synced_height against a trusted indexer (e.g. https://light.zcash.me/zns-testnet). They should converge. admin_pubkey and uivk should exactly match the values you pinned.
See Verifying State for a deeper walkthrough of cross-checking two indexers.
Hosted endpoints
If you don’t want to run your own, ZcashNames operates public endpoints:
| Network | URL |
|---|---|
| Testnet | https://light.zcash.me/zns-testnet |
| Mainnet (beta) | https://light.zcash.me/zns-mainnet-test |
These are the defaults used by the TypeScript SDK and the web app. For production applications that care about trust, running your own indexer is strongly recommended.
Operational notes
- Backups. The SQLite database is derived state - you can always re-sync from the chain. Backups are only useful if you want to avoid the re-sync cost.
- Upgrades. When upgrading the indexer across protocol changes, re-sync from scratch rather than trying to migrate. State derivation is cheap.
- Monitoring. Poll
/statusand alert onsynced_heightlag relative to lightwalletd’s chain tip. - Reorgs. The current indexer does not handle deep reorgs. For mainnet, consider adding rollback logic or accepting a confirmation depth before trusting state.