Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

ZAIR offers privacy-preserving tools for Zcash airdrops by allowing users to prove they own eligible notes on Zcash while preserving the privacy of the notes owned and the amounts claimed.

Security

ZAIR is a zero-knowledge proof that proves a user knows the spend-authorizing key for an eligible Sapling/Orchard note that existed and was unspent at a published snapshot height, and binds the claim to a specific (target-chain) intent message. ZAIR prevents double-claims by exposing an airdrop-specific nullifier while keeping note details and the standard Zcash nullifier private.

ZAIR does not ensure snapshot correctness: Claim soundness assumes the organizer's config.json (roots, parameters, and height) was computed from the intended Zcash chain state and published honestly. Verification soundness assumes verifiers implement strict decoding and all required checks; malformed inputs must be rejected.

This project has not been audited. See Security for security details.

How it works

ZAIR is a proof system between an airdrop organizer (who publishes a snapshot) and claimants (who generate claims from their wallet data). The system allows an organizer to receive signed messages that prove ownership of an eligible note at a snapshot, and a commitment to the value for binding.

  1. Trusted setup: if supporting proofs for Sapling (Groth16), a custom circuit is used requiring a trusted setup by the organizer, for example using multi-party computation. For Orchard, only a transparent setup is needed; either pre-computed or generated on-the-fly during proving.
  2. Organizer publishes a snapshot config: at a chosen snapshot height, the organizer builds and publishes config.json containing the Sapling/Orchard note commitment roots and spent-nullifier non-membership roots, plus per-pool parameters (e.g. target_id and value-commitment scheme). The organizer can also publish the snapshot nullifier lists (snapshot-*.bin) and optional prebuilt gap trees (gaptree-*.bin) derived from those lists.
  3. Claimant prepares claim inputs: using only a Unified Full Viewing Key (UFVK), the claimant scans for notes up to the snapshot height and constructs, for each eligible note, the private witness material needed for proving (note opening, membership path, and a gap-based non-membership witness against the snapshot’s spent-nullifier root). This produces a claim-prepared.json file and does not require spending keys.
  4. Claimant generates one ZK-proof per note: the proving step uses local spending key material (derived from a seed) to generate a Sapling or Orchard claim proof per eligible note. Each proof binds to the published snapshot roots, keeps the standard Zcash nullifier private, and exposes a domain-separated airdrop nullifier for double-claim prevention.
  5. Claimant signs a submission message: each proof is signed with a spend-authorizing signature over a digest that binds together the proof fields, the configured target_id, and a hash of an external message (the target-chain claim intent). This proves the claimant controls the spend-authorizing key and binds the proof claim to the intended message ("destination").
  6. Organizer / verifiers check and de-duplicate: verification consists of (1) checking the ZK proofs against config.json and (2) checking the spend-authorizing signatures and message binding. The verifier enforces one-time use by rejecting duplicate airdrop nullifiers.

Concepts overview

The system is built around key concepts:

  • roles & keys: organizer, claimant, and verifier roles, and the key material each requires.
  • snapshots: snapshot the chain at given height as source-of-truth for note balances.
  • non-membership proofs: to prove that a given note was not already spent.
  • airdrop nullifiers: to guard against double-claims by tracking notes already claimed.
  • value commitments: to bind the value in the note to the airdrop claim.

See Concepts for more details.

Airdrop proofs

The system provides concrete ZK-proofs using above concepts that integrate into Zcash. The proofs are for Groth16 (Sapling) and Halo2 (Orchard) respectively, that include design choices such as airdrop nullifier and gap-tree definitions, see Airdrop Proofs for more details.

Pipeline and CLI tool

The CLI is organized into these command groups:

  1. key: Derive seeds and viewing keys from a mnemonic.
  2. setup: Generate proving/verifying parameters (organizer/developer).
  3. config: Build and export snapshot configuration and artifacts.
  4. claim: Prepare, prove, and sign claims for eligible notes.
  5. verify: Verify claim proofs and their spend-authorizing signatures.

See CLI Reference for more details.

Integrations

ZAIR is designed to integrate with target chains that consume the signed claim submissions. The target chain verifies proofs and signatures, de-duplicates, and binds value commitments.

See Namada as example integration.

Getting Started

This section covers setting up the repository, building zair, and running a quick sanity check. Afterwards, follow the step-by-step guide in the CLI Reference.

Setup and Building

The repo includes a Nix flake that provides all dependencies (Rust, protoc):

nix develop
cargo build --release

Without Nix

Prerequisites

  • Rust 1.85.1+ (2024 edition)
  • Protobuf (protoc) for lightwalletd gRPC bindings

Build with:

cargo build --release

Sanity check

Verify the CLI is available:

./target/release/zair --help

and inspect the command groups:

./target/release/zair key --help
./target/release/zair setup --help
./target/release/zair config --help
./target/release/zair claim --help
./target/release/zair verify --help

Feature flags

The proving pipeline is gated behind the prove feature for some crates/binaries, enabled by default.

If you only need verification, you may build without proving support for a lighter dependency.

Concepts

This section describes central concepts for the airdrop system and proofs.

Roles & Keys

This page describes the roles and key considerations.

Roles

Organizer

The organizer sets up the airdrop by choosing a Zcash snapshot height, then fetch the on-chain nullifier sets, and build the airdrop configuration (config.json). This is the source-of-truth to be published, together with the verification key for custom Groth16 circuit if Sapling pools are enabled.

To avoid claimers needing to fetch nullifiers (e.g. via external services), the organizer will typically publish the configuration along with the nullifier snapshot. For further optimization, the organizer may also include the gap-tree hashes that will speed up by avoiding hashing the full non-membership tree when claiming. Finally, an organizer may also include (transparent) halo2 parameters to avoid the halo2 parameter generation when claiming.

The organizer does not need access to any user key material.

Claimant

The claimant is a Zcash user wanting to claim an airdrop by proving they own shielded unspent notes at the snapshot height. The claim pipeline involves three steps with different key requirements:

  1. Prepare (viewing key only): scans the chain for eligible notes at a given lightwalletd node.
  2. Prove (spending key): generates ZK proofs demonstrating note ownership.
  3. Sign (spending key): signs the proofs with a message binding the claim to an intent.

A claimer must trust that config.json does not have target ID Zcash_nf for Sapling and z.cash:Orchard for Orchard, otherwise the airdrop nullifier equals the standard note nullifier. More generally, a claimer should trust any software he provides his private spending key to.

Verifier

The verifier checks that submitted claims are valid. This requires the published config.json, verifying keys, and the signed submission. The verifier checks the signature and checks against double-claims by rejecting claims with airdrop nullifiers seen previously. The verifier then checks the eligible ownership proof, and finally verifies any external requirements on the message. For airdrops, a message would typically contain an external zero-knowledge proof binding the (commitment of the) value claimed to the eligible amount, that needs to to be verified successfully as well.

Keys

This project uses a hexadecimal seed-file as the root key to derive everything from. We offer tools to derive this from the standard mnemonic that most wallets can export. For the note preparation and scanning, only the unified viewing key ufvk is needed, that one can also derive via the CLI tool.

Key derivation CLI

The zair key commands help derive the keys needed for the claim pipeline:

# Derive seed from mnemonic
zair key derive-seed --mnemonic-file mnemonic.txt

# Derive UFVK from seed (for claim prepare)
zair key derive-ufvk --seed seed.txt --network testnet

See CLI Reference: key for details.

Security considerations

  • The seed and mnemonic are the root secrets. Anyone with access to them can spend all funds in the wallet. Never share them.
  • The UFVK allows viewing all transactions but cannot authorize spends. It is safe to use on a machine with network access (e.g. for scanning via lightwalletd).
  • The prove and sign steps require the seed. In a security-conscious setup, these should run on an isolated trusted machine producing and passing only the claim submission outside.
  • Intermediate files like claim-prepared.json and claim-proofs-secrets.json contain sensitive witness material. Treat them accordingly.

Concepts: Snapshots

An airdrop snapshot freezes eligibility to a Zcash block height on a network (e.g. mainnet).

Snapshot and roots

The snapshot is taken at a Zcash height (inclusive), and binds proofs to published roots:

  • Note commitment tree root: Used to prove the note exists.
  • Nullifier non-membership root: Used to prove the note was unspent.

Published artifacts

The organizer publishes:

  • config.json: the airdrop configuration including airdrop identifier and roots.
  • snapshot-{sapling,orchard}.bin: the sorted nullifier lists up to the snapshot height.
  • gaptree-{sapling,orchard}.bin: precomputed non-membership hash tree (optional).

Note

Claimants should treat config.json as the source-of-truth for the roots their proofs must verify against, and treat the .bin artifacts as untrusted until they reproduce configured roots.

Example

Below is an example config.json:

{
  "network": "testnet",
  "snapshot_height": 3839800,
  "sapling": {
    "note_commitment_root": "419a5213c91492aa4b14c5a976bc677088c8ce0b757573832b96b94ac3e08916",
    "nullifier_gap_root": "dcbc0747e877f57a4538bbd31bb0a523db7da5575b10a082b61c5d1c761eb53a",
    "target_id": "ZAIRTEST",
    "value_commitment_scheme": "sha256"
  },
  "orchard": {
    "note_commitment_root": "c426461167a9722175609ae899ae3c2a8a6edcb5c9b7f917622604c3813fd026",
    "nullifier_gap_root": "62c6c660493c1bb9cd541c8d66d45fca391dabf24afaf64506032227f4e61b08",
    "target_id": "ZAIRTEST:O",
    "value_commitment_scheme": "native"
  }
}

Concepts: Airdrop Nullifiers

To guard against airdrop double-claims while keeping claims unlinkable to later Zcash shielded spends, we publish an airdrop nullifier: a public, deterministic, airdrop-scoped identifier derived from the note’s nullifier key material via domain separation.

An airdrop configuration fixes two public domain parameters:

  • targetS: Sapling domain parameter (BLAKE2s personalization, exactly 8 bytes).
  • targetO: Orchard domain parameter used as the hash-to-curve domain string (at most 32 bytes, and must be valid UTF-8).

These parameters must be chosen so they do not coincide with the protocol domains for standard nullifiers: Zcash_nf (Sapling) and z.cash:Orchard (Orchard). See the cited specifications below.

Warning

If targetS = "Zcash_nf" (Sapling) or targetO = "z.cash:Orchard" (Orchard), then the airdrop construction collapses to the standard Zcash nullifier domain for that pool, meaning the published airdrop nullifiers will match Zcash nullifiers for the same notes and remove privacy.

Note

The codebase uses targetS = "ZAIRTEST" and targetO = "ZAIRTEST:O" as defaults.

  • Sapling: targetS is compiled into the Groth16 circuit; changing it requires a new trusted setup and verifying key, which is required for every target deployment supporting Sapling.
  • Orchard: targetO is baked into the circuit. Keys are re-derived deterministically per targetO at runtime (transparent setup, no ceremony).

Sapling

Sapling specifies the standard nullifier PRF (Sapling Protocol Specification, §5.4.2):

The Sapling airdrop nullifier uses the same construction, but replaces the personalization string with the public airdrop parameter targetS:

Orchard

Orchard specifies nullifier derivation (Zcash Protocol Specification, §4.16):

with generator:

The Orchard airdrop nullifier is derived by using an airdrop-specific generator:

and then computing as specified, but replacing with .

Concepts: Non-Membership Proofs

To claim an airdrop, a claimant must show their note existed at the snapshot and was unspent at the snapshot height. For shielded pools, "spent" is represented by a pool-specific nullifier set. We prove unspentness with a gap-based Merkle non-membership proof: prove the claimant’s (private) nullifier lies strictly between two consecutive spent nullifiers from the organizer’s snapshot.

Setup: spent-nullifier set and gaps

Let be the spent nullifiers observed on-chain up to the snapshot height (inclusive), sorted and de-duplicated by their canonical pool-specific ordering.

Here and are fixed sentinel bounds for the pool’s nullifier domain (Sapling uses and ; Orchard uses and the maximum valid field element ).

Define the open-interval gaps:

Here if and only if .

Merkle commitment to gaps

The gap tree commits to each adjacent pair by hashing a Merkle leaf using a pool-specific leaf hash with explicit domain separation (via a fixed "leaf level" distinct from internal-node levels).

The Merkle root of these leaves is published as the snapshot’s gap-root (one per pool).

In the current implementation, domain separation is provided by the hash personalization/level:

  • Sapling: is the Sapling Pedersen hash of the 512-bit string a || b (concatenation of two 32-byte nullifiers) using Merkle personalization level 62.
  • Orchard: is computed as a level-62 Orchard Merkle combine of the two canonical Orchard nullifier nodes corresponding to a and b.

Non-membership proof statement

Given the claimant’s private nullifier , the proof shows:

  1. Gap inclusion: opens to the published root via a standard Merkle authentication path.
  2. Strict interval: for the corresponding bounds , the circuit enforces .

Together this implies : if were equal to any spent nullifier in , it could not satisfy a strict inequality with the adjacent bounds.

Security notes

  • Edge cases: Since the outer gaps use fixed values and , nullifiers equal to either value cannot be used by this construction. We decided to accept this edge case given nullifiers are random and accepting them would imply further circuit constraints.
  • Field validity (Orchard): In addition to the gap argument, Orchard nullifiers must be checked to be valid encodings of field elements (e.g. ) separately from above.

Concepts: Value Commitments

This page gives the formal definitions used by ZAIR value commitments.

Native scheme

When the configured value-commitment scheme is native, each pool exposes its native Zcash value commitment, see the references and Zcash specification for details:

  • Sapling:
  • Orchard:

Here rcv is the randomness used by the pool-native commitment scheme.

SHA-256 scheme

When the configured value-commitment scheme is sha256, both Sapling and Orchard use:

Here rcv_sha256 is the randomness used by the SHA-256 commitment scheme, and is the value as 8 little endian bytes, and the prefix "Zair" is a fixed 4-byte ASCII domain-separation tag.

Airdrop Proofs

Our tool produces one ZK proof per eligible shielded note:

  • Sapling (Groth16 / bellman; adapted from Sapling Spend)
  • Orchard (Halo2; adapted from Orchard Action):

These are not Zcash spend proofs. They prove note ownership and snapshot eligibility without authorizing a transaction, and expose an airdrop-scoped nullifier for double-claim prevention.

What the ZK proof establishes (high level)

In-circuit (pool-specific details in sub-sections):

  • Ownership/spend consistency (recipient binding + public rk)
  • Snapshot inclusion (note membership of the note commitment root)
  • Snapshot unspentness (gap-tree non-membership against the spent-nullifier gap-root)
  • Airdrop nullifier derivation (public, domain-separated from Zcash nullifier)
  • Value binding via cv (native) or cv_sha256 (custom SHA-256 scheme)

Outside the circuit:

  • A spend-authorizing signature under rk binds the proof to an external message/context.

Crate layout

Each pool has two crates and one or more patched upstream dependencies:

SaplingOrchard
Circuitzair-sapling-circuitzair-orchard-circuit
Prover / verifierzair-sapling-proofszair-orchard-proofs
Patched depssapling-cryptoorchard, halo2-gadgets

The patches expose internal APIs (nullifier derivation with configurable domain, Pedersen/Sinsemilla hash internals, key types) so that host-side code can compute the same airdrop nullifiers and commitments that the circuits enforce. See the per-pool pages for details.

Key differences from standard Zcash spend circuits

Both circuits are closely aligned with the Sapling Spend Circuit and the Orchard Spend Action and reuse their note-integrity and ownership checks (recipient binding, rk derivation, note commitment, Merkle membership). The ZAIR-specific additions are:

Airdrop nullifier. The circuit computes two nullifiers from the same preimage: the standard Zcash nullifier (kept private) and an airdrop nullifier under a domain-separated derivation (exposed as a public input). Sapling uses BLAKE2s with a different personalization (b"ZAIRTEST"); Orchard uses a different hash-to-curve basepoint derived from a target_id string.

Gap-tree non-membership. Instead of exposing the real nullifier for double-spend prevention, the circuit proves that the private nullifier falls inside a gap in the sorted spent-nullifier set. The prover witnesses adjacent bounds (left < nf < right) and a Merkle path to the gap-tree root. Leaf hashing uses level 62 for domain separation from internal note-tree levels.

Unconditional note-anchor binding (Sapling). Upstream Sapling Spend skips anchor enforcement for zero-value dummy spends. ZAIR always enforces the note commitment root, so even zero-valued notes are bound to the snapshot.

SHA-256 value commitment (optional). When the sha256 scheme is selected, the circuit computes SHA256(b"Zair" || LE64(value) || rcv_sha256) and exposes the digest as public input instead of the native Pedersen commitment point. Orchard's SHA-256 mode uses the Table16 gadget from patched halo2-gadgets and requires K=17 (vs K=12 for native, and K=11 for standard spend).

Airdrop Proofs: Sapling

The Sapling claim proof is a Groth16 proof adapted from the Sapling Spend circuit.

Implementation:

  • Circuit: crates/zair-sapling-circuit/
  • Prover / verifier: crates/zair-sapling-proofs/

Patched dependency:

Proof statement

For a Sapling note, the circuit proves:

  • Ownership / spend-style consistency (recipient binding and public rk)
  • Snapshot inclusion: the note commitment opens to the note commitment root (anchor)
  • Snapshot unspentness: for the note’s private (standard) Sapling nullifier nf, the witness provides adjacent bounds (left, right) and the circuit enforces left < nf < right, plus a Merkle opening of the corresponding gap leaf to the gap-root
  • Double-claim prevention: a public airdrop nullifier is derived from the same nullifier preimage, using an airdrop-specific BLAKE2s personalization
  • Value binding: a public value commitment matches the note value (native cv or cv_sha256)

Public instance

The public input vector is defined by ClaimPublicInputs::to_vec in crates/zair-sapling-proofs/src/verifier/mod.rs. The length depends on the value commitment scheme:

  • Native: 8 BLS12-381 scalars

    1. rk.u, rk.v: randomization key
    2. cv.u, cv.v: value commitment (native)
    3. anchor: note commitment tree root
    4. hiding_nf: airdrop nullifier (256 bits multipacked into 2 scalars)
    5. nm_anchor: spent-nullifier gap-tree root
  • SHA-256: 8 BLS12-381 scalars

    1. rk.u, rk.v: randomization key
    2. cv_sha256: value commitment (256-bit digest multipacked into 2 scalars)
    3. anchor: note commitment tree root
    4. hiding_nf: airdrop nullifier (multipacked into 2 scalars)
    5. nm_anchor: spent-nullifier gap-tree root

The standard Zcash nullifier nf is computed in-circuit but is never a public input.

Private witness

Spend-style (reused from Sapling Spend):

  • ak, nsk: proof generation key, derives rk and nk
  • alpha (ar): randomization scalar for rk
  • g_d: from diversifier, used to derive pk_d
  • rcv: value commitment randomness
  • rcm: note commitment randomness
  • auth_path: Merkle path to anchor
  • value: note value

ZAIR-specific:

  • nm_left_nf, nm_right_nf: 32-byte gap bounds
  • nm_merkle_path: gap-tree Merkle path to nm_anchor.
  • rcv_sha256: 32-byte randomness (used only with the SHA-256 scheme)

Note

Standard Sapling Spend-circuits skips anchor equality when value = 0 via constraint:

We remove this feature and always require root equality:

Airdrop Proofs: Orchard

Orchard claim proofs are Halo2 proofs adapted from the Orchard Action spend circuit.

Implementation:

  • Circuit: crates/zair-orchard-circuit/
  • Prover / verifier: crates/zair-orchard-proofs/

Patched dependencies:

Proof statement

For an Orchard note, the circuit proves:

  • Ownership / spend-style consistency (recipient binding and public rk)
  • Snapshot inclusion: the note commitment opens to the note commitment root (note_anchor)
  • Snapshot unspentness: for the note’s private (standard) Orchard nullifier nf_old, the witness provides adjacent bounds (left, right) and the circuit enforces left < nf_old < right, plus a Merkle opening of the corresponding gap leaf to the gap-root
  • Double-claim prevention: a public airdrop nullifier is derived from the same nullifier preimage, using an airdrop-specific nullifier basepoint derived from target_id
  • Value binding: a public value commitment matches the note value (native cv or cv_sha256)

Public instance

The instance layout is defined by Instance::to_halo2_instance in crates/zair-orchard-circuit/src/circuit/airdrop.rs. The length depends on the value commitment scheme:

  • Native: 7 field elements

    1. rk.x, rk.y
    2. cv.x, cv.y
    3. note_anchor
    4. gap_root
    5. airdrop_nf
  • SHA-256: 13 field elements

    1. rk.x, rk.y
    2. cv_sha256 as 8 words (u32::from_be_bytes(digest[4i..4i+4]) for i=0..7)
    3. note_anchor
    4. gap_root
    5. airdrop_nf

The standard Zcash nullifier nf_old is computed in-circuit but is never a public input.

Private witness

Spend-style (adapted from Orchard Action):

  • g_d: diversified basepoint (recipient)
  • pk_d: diversified transmission key (recipient)
  • value: note value
  • rho: note randomness input
  • psi: note scalar (derived host-side from rseed + rho, witnessed in-circuit)
  • rcm: note: commitment trapdoor (derived host-side from rseed + rho, witnessed in-circuit)
  • note_pos: note commitment tree leaf position
  • note_path: note commitment tree Merkle path (siblings, leaf-to-root)
  • ak_p: spend authorizing key (point)
  • nk: nullifier deriving key (field element)
  • rivk: randomized incoming viewing key component (scalar)
  • alpha: randomizer for rk
  • rcv: native value commitment trapdoor (used when value_commitment_scheme = Native)
  • rcv_sha256: SHA-256 value commitment randomness (used when value_commitment_scheme = Sha256)

ZAIR-specific:

  • target_id: airdrop target id bytes
  • target_id_len: length of target_id in bytes
  • left: gap lower bound (enforce left < nf_old)
  • right: gap upper bound (enforce nf_old < right)
  • gap_pos: gap-tree leaf position
  • gap_path: gap-tree Merkle path (siblings, leaf-to-root)

CLI Reference

The zair CLI is organized into five command groups that mirror the airdrop pipeline:

Command groupRolePurpose
keyAnyoneDerive seed and viewing keys from a mnemonic
setupOrganizerGenerate proving/verifying parameters
configOrganizerBuild snapshot configuration from chain data
claimProverPrepare, prove, and sign airdrop claims
verifyVerifierVerify proofs and signatures

Step-by-step Guide

Below is a step-by-step guide for the full workflow:

0. Prerequisites

You need to have built the CLI tool, and a Zcash testnet wallet with shielded funds confirmed before or at the snapshot height. Use any Zcash wallet (e.g. zcash-cli or zingo-cli) to:

  1. Generate a new testnet account and export the mnemonic and birthday height.
  2. Obtain test notes from a faucet, for example testnet.zecfaucet.com.
  3. Obtain a few shielded Sapling or Orchard notes (or both).
  4. Wait for confirmation and note the confirmation height.

The tools will need the wallet birthday and the wallet mnemonic (with optional passphrase). For generating a snapshot, you need a snapshot height after the confirmed note height e.g. current.

1. Derive keys

Extract the seed from your mnemonic (sensitive!):

zair key derive-seed --mnemonic-file mnemonic.txt --no-passphrase

2. Generate parameters

Generate the trusted Sapling setup (required once per scheme):

zair setup sapling

Orchard setup is generated automatically during proving, but you may precompute it:

zair setup orchard

3. Build configuration

Build the airdrop snapshot configuration against a testnet height:

zair config build \
  --network testnet \
  --height <SNAPSHOT_HEIGHT>

This produces config.json, snapshot files, and gap-tree files.

4. Claim

Run the full claim pipeline (prepare, prove, sign) in one step:

zair claim run \
  --config config.json \
  --seed seed.txt \
  --birthday <WALLET_BIRTHDAY> \
  --message claim-message.bin

This produces:

  • claim-prepared.json: prepared proof inputs (sensitive!)
  • claim-proofs.json: generated proofs and public outputs
  • claim-proofs-secrets.json: local-only secrets (sensitive!)
  • claim-submission.json: signed submission for the target chain

5. Verify

Run the full verification pipeline to verify the submission (proofs + signatures):

zair verify run \
  --config config.json \
  --message claim-message.bin

The individual steps (claim prepare, claim prove, claim sign, verify proof, verify signature) can also be run separately. See their respective reference pages for details.

zair key

Key derivation utilities for the airdrop proving pipeline.

zair key derive-seed

Derives a 64-byte BIP-39 seed from a mnemonic and writes it as 128 hex characters to a file.

zair key derive-seed --mnemonic-file mnemonic.txt --no-passphrase --output seed.txt

zair key derive-ufvk

Derives a Unified Full Viewing Key (UFVK) from a seed file or mnemonic. The UFVK is mostly useful for claim prepare to scan for eligible notes without requiring spending authority. This would allow a user to outsource the claim preparation to a party who only holds the viewing key, not spend-keys.

zair key derive-ufvk --seed seed.txt --network testnet --output ufvk.txt

Exactly one of --seed, --mnemonic-file, or --mnemonic-stdin must be provided.

Note

The --account index must match the account used later in claim prove and claim sign.

zair setup

Generate proving and verifying parameters.

zair setup sapling

Generates Sapling Groth16 proving and verifying keys for the claim circuit.

zair setup sapling --scheme native

This outputs setup-sapling-pk.params and setup-sapling-vk.params.

Note

The circuit scheme must match config scheme used by config build --scheme-sapling xxx). Mismatched schemes will cause proof verification to fail.

zair setup orchard

Generates Orchard Halo2 parameters for proving and verification.

zair setup orchard --scheme native

This outputs setup-orchard-params.bin, as above, circuit scheme must match config.

Note

Orchard parameters can also be generated automatically during proving when --orchard-params-mode auto is set (default). Pre-generating can be useful for sharing or save computation.

zair config

Build the airdrop snapshot configuration from on-chain data.

zair config build

Connects to a lightwalletd node, fetches Sapling and/or Orchard nullifiers up to the snapshot height, builds gap trees for non-membership proofs, and writes the configuration and artifacts to files.

zair config build --network testnet --height 3663119

Parameters

FlagDefaultDescription
--networkmainnetNetwork: mainnet or testnet
--height(required)Height of snapshot
--lightwalletd(hardcoded)Endpoint for lightwalletd
--poolbothPool: sapling, orchard, or both

Airdrop parameters

FlagDefaultDescription
--target-saplingZAIRTESTSapling target ID for hiding nullifier derivation (exactly 8 bytes)
--target-orchardZAIRTEST:OOrchard target ID for hiding nullifier derivation (up to 32 bytes)
--scheme-saplingnativeSapling value commitment scheme: native or sha256
--scheme-orchardnativeOrchard value commitment scheme: native or sha256

Info

When choosing a custom --target-sapling for deployment, you must update the constant

HIDING_NF_PERSONALIZATION = "ZAIRTEST"

in

crates/zair-sapling-circuit/src/circuit.rs

as well, and run a trusted setup for Sapling using the new custom circuit.

Output files

FlagDefaultDescription
--config-outconfig.jsonConfiguration output
--snapshot-out-saplingsnapshot-sapling.binSapling snapshot nullifiers
--snapshot-out-orchardsnapshot-orchard.binOrchard snapshot nullifiers
--gap-tree-out-saplinggaptree-sapling.binSapling gap tree
--gap-tree-out-orchardgaptree-orchard.binOrchard gap tree
--no-gap-treefalseDo not output gap-tree artifacts

zair claim

Commands to prepare, prove, and sign airdrop claims; or run the full pipeline.

zair claim run

End-to-end claim pipeline: prepare, prove and sign.

zair claim run \
  --config config.json \
  --seed seed.txt \
  --birthday 3663119 \
  --message claim-message.bin

zair claim prepare

Scans the chain with a UFVK, finds eligible notes, and constructs the private witness material needed for proving. Does not require spending keys and can be outsourced to anyone with the viewing key.

zair claim prepare \
  --config config.json \
  --ufvk ufvk.txt \
  --birthday 3663119

zair claim prove

Generates one ZK proof per eligible note using the seed to derive spending keys.

zair claim prove \
  --config config.json \
  --seed seed.txt

Note

The --account index must match the one used to derive the UFVK in zair key derive-ufvk.

zair claim sign

Signs the generated proofs with spend-authorizing keys, binding each claim to a message payload.

zair claim sign \
  --config config.json \
  --seed seed.txt \
  --message claim-message.bin

Note

The prove and run subcommands require the prove feature (enabled by default). The prepare and sign subcommands are always available.

zair verify

Commands to verify a proof or signature; or run the verification for both.

zair verify run

End-to-end verification: verify proofs and signatures.

zair verify run \
  --config config.json \
  --submission-in claim-submission.json \
  --message claim-message.bin

Note

Verification does not require the prove feature and is lighter for target-chain integration.

zair verify proof

Verifies the ZK proofs in a proofs file against the airdrop configuration.

zair verify proof \
  --config config.json \
  --proofs-in claim-proofs.json

zair verify signature

Verifies spend-authorizing signatures in a signed claim submission.

zair verify signature \
  --config config.json \
  --submission-in claim-submission.json \
  --message claim-message.bin

Security

Audit and Status

Warning

This project is under active development and has not been externally audited.

Trust assumptions

  • Sapling trusted setup: the Sapling claim circuit uses Groth16, which requires a trusted setup to produce the proving and verifying keys. The zair setup sapling command generates keys locally for testing. A production deployment would need a trusted multi-party ceremony.
  • Orchard (no trusted setup): the Orchard claim circuit uses Halo2, which relies on a universal SRS (structured reference string) and does not require a per-circuit trusted setup.
  • lightwalletd: the config build and claim prepare steps connect to a lightwalletd node to fetch chain data. A malicious node could serve incorrect nullifier sets or note commitment trees. In production, the organizer should verify snapshot data against a trusted full node.

Privacy guarantees

The goal of the ZK proofs is protecting private information:

  • The Zcash nullifier of any claimed note.
  • The value of any claimed notes.
  • The note position of any claimed notes.

The following information is public per claim:

  • An airdrop-scoped nullifier.
  • A value commitment (native Pedersen or SHA256).
  • A randomized verification key.

Integrations

This section contains example integrations.

Integration: Namada

References