Signature Scheme
Every state-changing action in ZNS is authorized by an Ed25519 signature from the admin key. Every signed assertion the RPC returns carries its signature, so clients can verify without trusting the indexer.
This page is the normative reference for how signatures are constructed and verified. It mirrors the “Signature Verification” section of openrpc.json.
Algorithm
Ed25519 per RFC 8032 . No context string, no pre-hashing. Pure Ed25519.
Encoding
| Artifact | Format |
|---|---|
Public key (status.admin_pubkey) | 32 raw bytes, standard base64 with padding (44 chars) |
| Signature (all fields) | 64 raw bytes, standard base64 with padding (88 chars) |
| Pre-image | UTF-8 bytes of a literal ASCII string, no trailing newline |
Standard base64 as defined by RFC 4648 section 4 - the alphabet uses
+and/, not-and_. This is not base64url. ZIP-321 URIs use base64url for memo transport, but inside the memo the signature is standard base64.
Pre-image templates
Each action has a fixed pre-image that the signer encodes and signs. The signed bytes do not include the on-chain ZNS: memo prefix.
CLAIM:{name}:{address}
UPDATE:{name}:{address}:{nonce}
BUY:{name}:{address}
LIST:{name}:{price}:{nonce}
DELIST:{name}:{nonce}
RELEASE:{name}:{nonce}
SETPRICE:{count}:{tier_1}:…:{tier_count}:{nonce}Substitution rules:
{name}- verbatim from the registration (post-validation).{address}- the exact bech32m unified-address string returned inaddressorua. No normalization, no case folding, no whitespace trimming.{price},{nonce},{count},{tier_i}- decimal ASCII, no leading zeros, no sign, no padding. This is what Rust’s defaultu64 → Displayproduces.:- literal ASCII colon (0x3A) as the single delimiter.
All fields in every pre-image are ASCII-only, so UTF-8 encoding is byte-identical to the ASCII representation.
Verifying a registration
Given a Registration (from resolve or events) with name, address, nonce, last_action, and signature:
- Pick the pre-image template keyed on
last_action. ARegistration’slast_actionis always one ofCLAIM,UPDATE,DELIST, orBUY- neverLISTorRELEASE. - Substitute
name,address, andnonceverbatim.CLAIMandBUYdo not use the nonce;UPDATEandDELISTdo. - Encode the pre-image as UTF-8.
- Base64-decode
signatureto 64 bytes. - Base64-decode
status.admin_pubkeyto 32 bytes. - Run
Ed25519.verify(pubkey, preimage, signature). - Accept the registration only if verification succeeds.
Worked example. name = alice, address = u1example (shortened), nonce = 2, last_action = UPDATE.
Pre-image string: UPDATE:alice:u1example:2
UTF-8 bytes (24 bytes, hex):
55 50 44 41 54 45 3a 61 6c 69 63 65 3a 75 31 65 78 61 6d 70 6c 65 3a 32Feed those bytes, the decoded signature, and the decoded admin_pubkey to any standards-compliant Ed25519 verifier.
Verifying a listing
A Listing carries its own signature over LIST:{name}:{price}:{nonce}. This is independent of the enclosing registration’s signature. To trust the claim “alice is listed at 1 ZEC” you must verify both:
- The registration signature (proves ownership).
- The listing signature (proves the ownership-holder authorized this specific price).
They cover different facts. Verifying only one is not enough.
Verifying events
Every Event returned by the events method carries its own signature. The pre-image template is selected by action:
action | Pre-image | Fields used |
|---|---|---|
| CLAIM | CLAIM:{name}:{ua} | name, ua |
| UPDATE | UPDATE:{name}:{ua}:{nonce} | name, ua, nonce |
| BUY | BUY:{name}:{ua} | name, ua |
| LIST | LIST:{name}:{price}:{nonce} | name, price, nonce |
| DELIST | DELIST:{name}:{nonce} | name, nonce |
| RELEASE | RELEASE:{name}:{nonce} | name, nonce |
| SETPRICE | SETPRICE:{count}:{t_1}:…:{t_count}:{nonce} | - (see below) |
SETPRICE events are a special case: the full tier list is not exposed as a structured field on the Event object. price is null, nonce is the SETPRICE nonce, and the signature is present - but you cannot reconstruct the pre-image from the event alone. To fully verify a SETPRICE, scan the original memo from the on-chain note using the UIVK, or treat events as “the indexer claims this signature exists” without independent verification.
Verifying pricing (the trust caveat)
status.pricing is not accompanied by a signature. The admin does sign every SETPRICE memo on-chain, but /status flattens the result without re-attaching the signature. Clients that accept status.pricing as-returned are trusting the indexer for pricing data.
Trust-minimized options:
- Query
eventsfiltered to the most recentSETPRICE, then scan the on-chain memo for the raw tier list and reconstruct the pre-image. - Run your own indexer. See Verifying State.
Replay protection
The nonce is per-name (for name-scoped actions) or global (for SETPRICE). It is a u64 stored in the registry and must strictly increase across successive actions. The indexer rejects any memo whose nonce is ≤ the current value.
Nonce arithmetic:
- CLAIM sets
nonce = 0. - UPDATE, LIST, DELIST, RELEASE require
nonce > current. The incoming value becomes the new current. - BUY resets
nonceto0because the name has a new owner, invalidating any signed memos queued by the previous owner.
A safe client pattern: resolve(name) → read nonce → sign with nonce + 1. Gaps are permitted but wasteful.
Key management
The signing key lives outside the indexer. In the reference web app (zcashnames.com) it is held by a server process that accepts signing requests from server actions - see lib/zns/admin.ts. The indexer only ever sees the public half via the ZNS_ADMIN_PUBKEY environment variable.
Rotating the admin key is an unplanned, destructive operation. Every registration signed by the old key would fail verification against the new key. There is currently no on-chain rotation message - rotation would require coordinating a new indexer deployment with the new key.
Reference implementation
Rust: src/memo.rs::verify_signature in the indexer repo.
TypeScript: not currently exposed from the SDK (verification is not done client-side in the web app). The SDK emits memos from a signed payload it receives from the server. A future SDK addition will add a verifyRegistration(reg, adminPubkey) helper.