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.
- 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.
- Organizer publishes a snapshot config: at a chosen snapshot height, the organizer builds and
publishes
config.jsoncontaining the Sapling/Orchard note commitment roots and spent-nullifier non-membership roots, plus per-pool parameters (e.g.target_idand 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. - 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.jsonfile and does not require spending keys. - 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.
- 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"). - Organizer / verifiers check and de-duplicate: verification consists of (1) checking the
ZK proofs against
config.jsonand (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:
key: Derive seeds and viewing keys from a mnemonic.setup: Generate proving/verifying parameters (organizer/developer).config: Build and export snapshot configuration and artifacts.claim: Prepare, prove, and sign claims for eligible notes.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
With Nix (recommended)
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:
- Prepare (viewing key only): scans the chain for eligible notes at a given lightwalletd node.
- Prove (spending key): generates ZK proofs demonstrating note ownership.
- 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.jsonandclaim-proofs-secrets.jsoncontain 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).
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.
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.
The codebase uses targetS = "ZAIRTEST" and targetO = "ZAIRTEST:O" as defaults.
- Sapling:
targetSis 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:
targetOis baked into the circuit. Keys are re-derived deterministically pertargetOat 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
aandb.
Non-membership proof statement
Given the claimant’s private nullifier , the proof shows:
- Gap inclusion: opens to the published root via a standard Merkle authentication path.
- 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) orcv_sha256(custom SHA-256 scheme)
Outside the circuit:
- A spend-authorizing signature under
rkbinds the proof to an external message/context.
Crate layout
Each pool has two crates and one or more patched upstream dependencies:
| Sapling | Orchard | |
|---|---|---|
| Circuit | zair-sapling-circuit | zair-orchard-circuit |
| Prover / verifier | zair-sapling-proofs | zair-orchard-proofs |
| Patched deps | sapling-crypto | orchard, 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:
sapling-crypto/from https://github.com/eigerco/sapling-crypto
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
rk.u,rk.v: randomization keycv.u,cv.v: value commitment (native)anchor: note commitment tree roothiding_nf: airdrop nullifier (256 bits multipacked into 2 scalars)nm_anchor: spent-nullifier gap-tree root
-
SHA-256: 8 BLS12-381 scalars
rk.u,rk.v: randomization keycv_sha256: value commitment (256-bit digest multipacked into 2 scalars)anchor: note commitment tree roothiding_nf: airdrop nullifier (multipacked into 2 scalars)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, derivesrkandnkalpha(ar): randomization scalar forrkg_d: from diversifier, used to derivepk_drcv: value commitment randomnessrcm: note commitment randomnessauth_path: Merkle path toanchorvalue: note value
ZAIR-specific:
nm_left_nf,nm_right_nf: 32-byte gap boundsnm_merkle_path: gap-tree Merkle path tonm_anchor.rcv_sha256: 32-byte randomness (used only with the SHA-256 scheme)
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:
orchard/from https://github.com/eigerco/orchardhalo2-gadgets/from https://github.com/eigerco/halo2
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
rk.x,rk.ycv.x,cv.ynote_anchorgap_rootairdrop_nf
-
SHA-256: 13 field elements
rk.x,rk.ycv_sha256as 8 words (u32::from_be_bytes(digest[4i..4i+4])for i=0..7)note_anchorgap_rootairdrop_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 valuerho: note randomness inputpsi: 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 positionnote_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 rkrcv: 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 bytestarget_id_len: length of target_id in bytesleft: gap lower bound (enforce left < nf_old)right: gap upper bound (enforce nf_old < right)gap_pos: gap-tree leaf positiongap_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 group | Role | Purpose |
|---|---|---|
key | Anyone | Derive seed and viewing keys from a mnemonic |
setup | Organizer | Generate proving/verifying parameters |
config | Organizer | Build snapshot configuration from chain data |
claim | Prover | Prepare, prove, and sign airdrop claims |
verify | Verifier | Verify 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:
- Generate a new testnet account and export the mnemonic and birthday height.
- Obtain test notes from a faucet, for example testnet.zecfaucet.com.
- Obtain a few shielded Sapling or Orchard notes (or both).
- 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 outputsclaim-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.
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.
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.
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
| Flag | Default | Description |
|---|---|---|
--network | mainnet | Network: mainnet or testnet |
--height | (required) | Height of snapshot |
--lightwalletd | (hardcoded) | Endpoint for lightwalletd |
--pool | both | Pool: sapling, orchard, or both |
Airdrop parameters
| Flag | Default | Description |
|---|---|---|
--target-sapling | ZAIRTEST | Sapling target ID for hiding nullifier derivation (exactly 8 bytes) |
--target-orchard | ZAIRTEST:O | Orchard target ID for hiding nullifier derivation (up to 32 bytes) |
--scheme-sapling | native | Sapling value commitment scheme: native or sha256 |
--scheme-orchard | native | Orchard value commitment scheme: native or sha256 |
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
| Flag | Default | Description |
|---|---|---|
--config-out | config.json | Configuration output |
--snapshot-out-sapling | snapshot-sapling.bin | Sapling snapshot nullifiers |
--snapshot-out-orchard | snapshot-orchard.bin | Orchard snapshot nullifiers |
--gap-tree-out-sapling | gaptree-sapling.bin | Sapling gap tree |
--gap-tree-out-orchard | gaptree-orchard.bin | Orchard gap tree |
--no-gap-tree | false | Do 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
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
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
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
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 saplingcommand 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 buildandclaim preparesteps 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.