Skip to Content
ProtocolSignature Scheme

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

ArtifactFormat
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-imageUTF-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 in address or ua. 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 default u64 → Display produces.
  • : - 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:

  1. Pick the pre-image template keyed on last_action. A Registration’s last_action is always one of CLAIM, UPDATE, DELIST, or BUY - never LIST or RELEASE.
  2. Substitute name, address, and nonce verbatim. CLAIM and BUY do not use the nonce; UPDATE and DELIST do.
  3. Encode the pre-image as UTF-8.
  4. Base64-decode signature to 64 bytes.
  5. Base64-decode status.admin_pubkey to 32 bytes.
  6. Run Ed25519.verify(pubkey, preimage, signature).
  7. 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 32

Feed 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:

actionPre-imageFields used
CLAIMCLAIM:{name}:{ua}name, ua
UPDATEUPDATE:{name}:{ua}:{nonce}name, ua, nonce
BUYBUY:{name}:{ua}name, ua
LISTLIST:{name}:{price}:{nonce}name, price, nonce
DELISTDELIST:{name}:{nonce}name, nonce
RELEASERELEASE:{name}:{nonce}name, nonce
SETPRICESETPRICE:{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:

  1. Query events filtered to the most recent SETPRICE, then scan the on-chain memo for the raw tier list and reconstruct the pre-image.
  2. 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 nonce to 0 because 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.

Last updated on