This proof uses a forked Orchard Action circuit. It has not been reviewed. It has run on the Zcash public testnet and on a private regtest chain, and has been attacked with dishonest provers, byte-by-byte edits and cross-use (see status), but no external cryptographer has looked at it. See forked circuits.
Two questions need the same primitive:
  • Claims. One claim per wallet in an airdrop, grant round or vote.
  • Distinct payers. A donation round counts wallets, so one donor splitting a gift into ten payments is one donor.

The tag

A tag is Poseidon(nk, domain), where nk is the note’s nullifier deriving key and domain is the round’s name. In the circuit the same nk cell feeds both the address check (ivk = CommitIvk(ak, nk, rivk)) and the tag hash. So:
  • Every claim one key makes in one domain carries one tag, whatever note it uses. Every address and both scopes of an account share nk.
  • Finding a second tag for a key would need a CommitIvk collision.
  • Tags in different domains are unrelated values, as Poseidon keyed by nk is a PRF.
  • A claim and a payer tag hash the domain under different personalizations, so a key’s tag in “drop” as a claim and as a payer tag don’t match. (They once did: a round named after someone else’s airdrop could have matched donors to claimants.)

Claim tags

Shows a note of at least floor was held in the tree at a snapshot block, and carries the tag.
The claimant picks the floor the claim shows, so you must say the least you accept with --min (the API takes min_floor_zatoshis). Without it a note of one zatoshi would prove “at least 1” and verify. Against a chain snapshot the claim reveals the note’s nullifier, and the verifier checks it was not published up to the snapshot block. The tree keeps spent notes, so without that check a balance could claim from each key it moved through. Two claims from one wallet in one domain carry the same tag, which is how the second is caught. The regtest end-to-end run does exactly this, and checks a claim is refused for another domain or another snapshot block.

Payer tags

A payer tag is the same proof on a note a payment spent: its nullifier is one of the payment’s, its anchor is the payment’s.
Different tags mean different paying keys. A round that wants k different wallets collects one tag per payment and counts distinct values.

Combining with income

Proof of income accepts payer tags: a lender can require that a total came from at least k different wallets.
If a payer shows a sibling-account proof for two of the tags, add --siblings @sib.txt and the two count as one wallet.

Limits

  • Tags count wallets at most, never at least. A person with two accounts of one wallet gets two tags, and a sibling-account proof merges them only if the payer chooses to show it: you can’t make anyone show one. Nothing at all catches independent wallets.
  • Claims against a published list (not a chain snapshot) use a second key mode where the nullifier stays private, since there is no chain to check it against.
Code: crates/zceipt-core/src/claim.rs, circuit in vendor/orchard/src/circuit/claim.rs. Tests: roundtrip.rs::claims_are_one_per_key_per_domain, ::payer_tags_tell_wallets_apart.