Pre-launch testingMainnet live in Until then the site runs on test data for you to try every flow — no real funds are held.Launch status
ZECVAULT.FUN

zecvault/v0.1-reference

Technical architecture

What happens on Zcash, what happens off-chain, and exactly which parts you have to trust.

Division of responsibilities

LayerResponsible forNotes
Zcash networkValue transfer and privacy of amounts, senders, and receivers.Shielded deposits carry a commitment in the encrypted memo. Zcash has no general smart contracts — it does not run draw logic.
IndexerDetecting confirmed deposits using the draw’s incoming viewing key.Requires a configurable confirmation depth. Reorgs before close roll entries back.
Coordinator (off-chain)Snapshot freezing, seed reveal, winner selection, claim intake.Trusted for liveness and censorship resistance, not for correctness — every output is publicly recomputable.
Verifiers (anyone)Recomputing roots, randomness, winners, and the treasury balance.Runs in the browser on the Proof Explorer, or offline from the published JSON.
SettlementShielded payouts to claim-bound addresses.Signed by the operator’s spending key. Payout txids are published only once confirmed.

1. Entry commitments and nullifiers

A participant generates a credential locally: a 32-byte secret and a 32-byte nullifierKey. Only the commitment is published. All hashes are domain-separated SHA-256 (@noble/hashes).

lib/protocol/commitments.ts
H_tag(x)   = SHA256(SHA256("zecvault/v0/" ‖ tag) ‖ SHA256("zecvault/v0/" ‖ tag) ‖ x)
commitment = H_commitment(drawId ‖ secret ‖ nullifierKey)
nullifier  = H_nullifier(drawId ‖ nullifierKey)

2. Eligibility and duplicate-entry prevention

An entry counts only if a confirmed shielded deposit of the required amount carries the memo ZV1:<drawId>:<commitment> before close. The indexer deduplicates by commitment — a commitment can appear in a snapshot at most once, so replayed memos add nothing.

3. Draw closure and entry snapshots

At close, accepted commitments are ordered by first confirmation and frozen into a binary Merkle tree with distinct leaf and node tags (defeating second-preimage tricks). The root is published before any randomness exists.

4. Verifiable randomness

Before a draw opens, the operator publishes H(seed). The randomness also mixes in a public beacon round (drand) that occurs after close, so neither the operator nor participants can predict or grind the outcome. Winners are sampled without replacement using rejection sampling to avoid modulo bias.

lib/protocol/randomness.ts
seedCommitment = H_randomness/seed-commit(seed)            // published before open
randomness     = H_randomness/draw(seed ‖ beacon ‖ snapshotRoot)
sample_k       = H_randomness/winner(randomness ‖ k)        // rejection-sampled, distinct

5. Zero-knowledge proof verification

The reference build verifies claims with a Merkle path, which reveals which snapshot entry won. The roadmap replaces this with a zk-SNARK (Halo 2) proving “I know a secret opening a winning leaf, and this is its nullifier” without revealing the leaf. This is not implemented yet and no page claims otherwise.

6. Private claims and replay protection

A claim reveals the nullifier and binds it to a fresh shielded payout address via H(nullifier ‖ drawId ‖ address). Production stores spent nullifiers under a unique constraint, inserted in the same database transaction that queues the payout, so concurrent replays fail atomically.

7. Treasury reconciliation and settlement recovery

The ledger is re-summed and compared with an independent viewing-key scan. A mismatch halts new draws. Payouts are idempotent per nullifier: if a broadcast fails or is reorged out, the payout is retried from the same record and never duplicated.

Assumptions

  • SHA-256 is collision- and preimage-resistant.
  • The drand network is not fully compromised.
  • The operator stays online to settle — it can delay, but cannot alter, a verifiable outcome.
  • Zcash shielded pools hide amounts and parties; timing analysis of deposits is still possible.