Skip to Content
Indexer & RPCVerifying State

Verifying State

The claim “ZNS is trust-minimized” only holds if clients actually verify what they get back from the RPC. This page walks through the three verification layers in order of increasing paranoia. For the formal trust assumptions this verification rests on, see the Trust model.

For a no-code visual spot-check, the Explorer renders the indexer’s events, registrations, and listings in a single UI. It’s not a substitute for cryptographic verification - it talks to the same hosted indexer you’d be verifying - but it’s the fastest way to confirm a transaction landed and the registry sees it.

Layer 1: pin the UIVK and admin pubkey

At minimum, a client should pin the expected uivk and admin_pubkey and refuse to proceed if the indexer reports different values.

import { createClient } from "zcashname-sdk"; // createClient calls status() under the hood and throws if the returned // uivk isn't in KNOWN_UIVKS (testnet + mainnet pinned) const client = await createClient("https://light.zcash.me/zns-testnet");

The SDK’s createClient does this automatically. A custom client should replicate the check:

const s = await rpc("status"); if (s.uivk !== EXPECTED_UIVK) throw new Error("UIVK mismatch"); if (s.admin_pubkey !== EXPECTED_ADMIN_PUBKEY) throw new Error("admin pubkey mismatch");

What this catches: a wrong or malicious indexer pointing you at a different registry wallet. A MITM who forwards your requests to a fake indexer would have to forge a status response; pinning the UIVK and admin pubkey makes that immediately detectable.

What this does not catch: a legit indexer that lies about individual resolve/events responses. It has the correct UIVK and pubkey, but it might return fake registrations. That’s Layer 2.

Layer 2: verify every signature

Every Registration, Listing, and Event carries an Ed25519 signature. A client that cares about correctness should verify each one against the pinned admin_pubkey before acting on the data.

Pseudocode (the SDK does not yet expose this helper, but it’s not hard):

import { verify } from "@noble/ed25519"; import { resolve } from "zcashname-sdk"; const reg = await resolve("alice"); if (reg === null) { /* not found */ return; } const preimage = (() => { switch (reg.last_action) { case "CLAIM": return `CLAIM:${reg.name}:${reg.address}`; case "UPDATE": return `UPDATE:${reg.name}:${reg.address}:${reg.nonce}`; case "BUY": return `BUY:${reg.name}:${reg.address}`; case "DELIST": return `DELIST:${reg.name}:${reg.nonce}`; } })(); const sig = Buffer.from(reg.signature!, "base64"); const pub = Buffer.from(EXPECTED_ADMIN_PUBKEY, "base64"); const ok = await verify(sig, new TextEncoder().encode(preimage), pub); if (!ok) throw new Error("Registration signature invalid"); // If the registration is listed, verify the listing signature too. if (reg.listing) { const lp = `LIST:${reg.listing.name}:${reg.listing.price}:${reg.listing.nonce}`; const ls = Buffer.from(reg.listing.signature, "base64"); const lok = await verify(ls, new TextEncoder().encode(lp), pub); if (!lok) throw new Error("Listing signature invalid"); }

For the full template list see Signature Scheme.

What this catches: an indexer that fabricates registrations, listings, or events. Without the admin’s private key, no attacker can produce a signature that verifies under the pinned pubkey.

What this does not catch: an indexer that omits data. If a name was claimed on chain but your indexer pretends it doesn’t exist, Layer 2 can’t detect it - there’s nothing to verify.

Layer 3: run your own indexer

Run the indexer yourself against the same UIVK and admin pubkey. The SQLite database is a pure function of (chain, UIVK, admin_pubkey), so your instance will converge on exactly the same state as any other correctly configured indexer.

Cross-check by comparing counts:

# Hosted curl -sX POST https://light.zcash.me/zns-testnet \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"status","params":{}}' \ | jq -r '.result | "\(.synced_height)\t\(.registered)\t\(.listed)"' # Local curl -sX POST http://localhost:3000 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"status","params":{}}' \ | jq -r '.result | "\(.synced_height)\t\(.registered)\t\(.listed)"'

At the same synced_height, registered and listed should be identical.

If you want even stronger assurance, diff individual registrations:

for name in alice bob carol; do a=$(curl -sX POST https://light.zcash.me/zns-testnet -H "Content-Type: application/json" \ -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"resolve\",\"params\":{\"query\":\"$name\"}}" \ | jq -Sc .result) b=$(curl -sX POST http://localhost:3000 -H "Content-Type: application/json" \ -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"resolve\",\"params\":{\"query\":\"$name\"}}" \ | jq -Sc .result) [ "$a" = "$b" ] && echo "ok $name" || { echo "diff $name"; diff <(echo "$a") <(echo "$b"); } done

What this catches: everything. Omissions, fabrications, stale data, misreported pricing. If your own indexer and the hosted one disagree at the same synced_height, one of them is wrong - and yours is the one built directly from the chain.

The pricing caveat

status.pricing is not signed on the status endpoint. A malicious indexer could report tiers that don’t match any on-chain SETPRICE memo. Layer 1 and Layer 2 do not catch this.

To verify pricing trust-minimally:

  1. Query events filtered to SETPRICE, sorted newest first.
  2. Treat the signature on the most recent SETPRICE event as “the indexer claims this signature exists over some tier list with this nonce”. The tier list itself is not structured on the event, so you cannot reconstruct the pre-image from the event alone.
  3. To actually verify, scan the on-chain memo body for that event’s txid using the UIVK, reconstruct the full pre-image SETPRICE:{count}:{t_1}:…:{t_count}:{nonce}, and verify against the signature.

The simpler path is Layer 3 - run your own indexer, which derives pricing from the chain the same way. The unsigned status.pricing field becomes irrelevant when you’re the one computing it.

Summary

LayerCatchesCostRecommended for
1. Pin UIVK + pubkeyMITM, wrong indexer~freeEvery client
2. Verify signaturesFabricated dataA few ms per responseWallets, explorers, SDKs
3. Run your own indexerOmissions, pricing liesOps burdenProduction services, exchanges, anyone with skin in the game
Last updated on