💃 Dance & Choreography · dance contests

Competition Eligibility Proof

Competition Eligibility Proof proves membership of a dance contests roster against a public Merkle root without revealing which member.

Set Membership· in the list, without saying which row
Section · Noir

The primitive.

full primer →

Choreographers prove they are on the dance contests roster against a single public root — membership confirmed without naming which entry they are, and without handing over the list.

Why this primitiveSet membership matches dance contests in Dance & Choreography because the real question is whether someone belongs to a roster, guild or allowlist — and answering it by row number exposes both the person and the whole list.

Primitive
publish a Merkle root of the allowed set, then prove a private leaf and its private sibling path hash up to that public root. The circuit recomputes the path and asserts equality with the root, so membership is proved while the identity of the member — and the size and order of the set — stay private
Drives the UI as
a door, allowlist or roster check where the visitor proves they are on the list without revealing which entry they are, and the venue never receives a copy of the list membership
Appendix · Build mode

Pick how the proof is generated.

Start here. It is the only mode that reliably fits a one-shot, 5-credit Lovable build, needs no accounts or secrets, and it is the most convincing demo: the judge watches their own machine produce the proof.

Appendix · Secrets

No keys required.

The browser prover runs entirely inside the app — Noir compiles and Barretenberg proves client-side. Paste the prompt and build; nothing to configure.

Switch to a native or on-chain mode above when the proof has to be verified outside the browser.

Appendix · Mega-prompt

The build prompt.

Paste into a fresh Lovable project. The prompt below is written for the Browser prover mode. read the build strategy →

Build "Competition Eligibility Proof" as a ONE-SHOT Lovable build.
Single-page TanStack Start app with a real Noir zero-knowledge proof. Cut scope ruthlessly.

CONCEPT
Participants can prove their eligibility for competitions without disclosing their identities or personal details to organizers.
Discipline: Dance & Choreography (dance contests).
Recipe: Set Membership (in the list, without saying which row) as the single Noir primitive.
Why zero-knowledge: Set membership matches dance contests in Dance & Choreography because the real question is whether someone belongs to a roster, guild or allowlist — and answering it by row number exposes both the person and the whole list.

LOVABLE BUDGET (HARD CAP: ONE-SHOT, ~5 CREDITS TOTAL):
This prompt MUST ship a working demo on the FIRST message with zero follow-ups.
- ONE TanStack Start app, ONE route (`src/routes/index.tsx`). No auth, no nav.
- ONE circuit module `src/lib/circuit.ts` holding the Noir source as a string.
- ONE client-only prover component wired to a form + result panel.
- NO database, NO Lovable Cloud, NO server functions, NO file uploads.
  The whole proof happens in the browser; there are no secrets to hide.
- Libraries: template defaults + the three proving packages below. Nothing else.
- If a feature is not in the user flow below, do not build it.

NOIR PRIMER (assume the reader has never written a circuit)
- Noir is a Rust-like DSL for zero-knowledge circuits. You write assertions over
  private inputs; the prover produces a succinct proof that they hold; a verifier
  checks the proof without seeing the inputs. Docs: https://noir-lang.org/docs/
- Function parameters are PRIVATE by default. Mark a parameter `pub` to make it
  public. The return value is ALWAYS public.
- `assert(...)` is the security statement. Anything your UI claims must map to an
  assert or to a value the circuit returns.
- Proving here uses Barretenberg's UltraHonk backend (`@aztec/bb.js`), the same
  backend `bb` uses on the command line.
- Fields are BN254 (~254 bits). A 256-bit hash does NOT fit in one Field — split
  it into two 128-bit limbs.
- Nothing is uploaded and nothing is stored: the private input never leaves the
  page, so this build needs no backend at all.

RUNTIME CONVENTIONS (this template — follow exactly)
- Stack: TanStack Start v1 + React 19 + Vite 7 + Tailwind v4 (`src/styles.css`).
  There is NO `src/App.tsx`, no react-router-dom, no `src/pages`.
- Routes live in `src/routes`; the home page is `src/routes/index.tsx` and uses
  `createFileRoute("/")({ component: Page, head: () => ({ meta: [...] }) })`.
  Never edit `src/routeTree.gen.ts`.
- SSR runs every module import. The prover must live in a CLIENT-ONLY entry:
  `<ClientOnly>` + `React.lazy(() => import("@/components/prover-entry"))`, and the
  three proving packages must be imported INSIDE the handler with `await import(...)`,
  never at module scope. `crypto.subtle`, `PerformanceObserver` and WebAssembly
  streaming do not exist during SSR.
- Never name an SSR-reachable module `*.client.*` — the SSR build rejects the
  pattern even behind a dynamic import.
- TypeScript runs with `exactOptionalPropertyTypes`: pass optional props as
  `...(x ? { prop: x } : {})`, not `prop: x ?? undefined`.
- Toasts: `sonner` (`import { toast } from "sonner"`), and render `<Toaster />`
  once in `src/routes/__root.tsx`. `@/hooks/use-toast` does NOT exist here.
- Colours come from semantic tokens in `src/styles.css` — no hardcoded
  `text-white` / `bg-black` / `bg-[#hex]` in components.
- Give `src/routes/index.tsx` its own `head()` with a real title and description.

PACKAGES AND VITE CONFIG (copy this exactly — it is the whole trap)
Install:
  @noir-lang/noir_wasm@1.0.0-beta.26   # Noir source -> ACIR
  @noir-lang/noir_js@1.0.0-beta.26     # witness generation
  @aztec/bb.js@5.1.0                   # UltraHonk prove/verify

vite.config.ts:
```ts
import { defineConfig } from "vite";
import path from "node:path";
import fs from "node:fs";

/** Serve noir_wasm's prebuilt browser bundle verbatim; the bundler must not touch it. */
function noirWasmVendorAsset() {
  const file = path.resolve("node_modules/@noir-lang/noir_wasm/web/main.mjs");
  const url = "/vendor/noir_wasm/main.mjs";
  return {
    name: "noir-wasm-vendor-asset",
    configureServer(server) {
      server.middlewares.use(url, (_req, res) => {
        res.setHeader("content-type", "text/javascript");
        res.end(fs.readFileSync(file));
      });
    },
    generateBundle() {
      this.emitFile({
        type: "asset",
        fileName: "vendor/noir_wasm/main.mjs",
        source: fs.readFileSync(file),
      });
    },
  };
}

export default defineConfig({
  plugins: [noirWasmVendorAsset() /* , ...template plugins */],
  optimizeDeps: {
    exclude: ["@noir-lang/noir_wasm", "@noir-lang/noir_js", "@aztec/bb.js"],
  },
});
```
Load the compiler through that URL so nothing rewrites it:
```ts
const noirWasm = await import(/* @vite-ignore */ "/vendor/noir_wasm/main.mjs") as {
  compile: (fm: unknown) => Promise<unknown>;
  createFileManager: (root: string) => { writeFile: (p: string, s: ReadableStream) => Promise<void> };
};
```

MODE — BROWSER PROVER (compile and prove client-side, no secrets, no backend)
Everything runs in the visitor's browser. Nothing is uploaded; there is no server
function, no database and no API key anywhere in this build.

PACKAGES (pin these exact versions together — a skew breaks witness generation)
  @noir-lang/noir_wasm  1.0.0-beta.26   # Noir source -> ACIR
  @noir-lang/noir_js    1.0.0-beta.26   # witness generation
  @aztec/bb.js          5.1.0           # Barretenberg UltraHonk prove/verify

VITE CONFIG — BOTH RULES ARE MANDATORY
1) Never let the dep optimizer pre-bundle the proving stack:
     optimizeDeps: { exclude: ["@noir-lang/noir_wasm", "@noir-lang/noir_js", "@aztec/bb.js"] }
   esbuild rewrites the wasm-bindgen glue; the dynamic import still resolves, then
   wasm instantiation HANGS WITH NO ERROR. A step that never completes and never
   throws is always this.
2) Serve noir_wasm's published browser bundle verbatim. Rolldown re-processing it
   emits `const Infinity = Infinity` inside its vendored @ltd/j-toml, so importing
   the compiler throws "Cannot access 'Infinity' before initialization" (minified:
   "Cannot access 'j' before initialization"). PRODUCTION ONLY — preview looks fine
   and the published site dies. Add a small Vite plugin that exposes
   node_modules/@noir-lang/noir_wasm/web/main.mjs at /vendor/noir_wasm/main.mjs
   (dev: server.middlewares; build: this.emitFile in generateBundle, client env
   only) and load it with:
     import(/* @vite-ignore */ "/vendor/noir_wasm/main.mjs")

SSR
The compiler, crypto.subtle, PerformanceObserver and WebAssembly streaming are all
browser-only. Put the prover in a client-only entry: <ClientOnly> + React.lazy, and
import the three packages INSIDE the handler, never at module scope.

COMPILE IN THE BROWSER
  const fm = createFileManager("/");
  await fm.writeFile("./src/main.nr", new Blob([CIRCUIT_SOURCE]).stream());
  await fm.writeFile("./Nargo.toml", new Blob([NARGO_TOML]).stream());
  const compiled = await compile(fm);
  const program = "program" in compiled ? compiled.program : compiled; // beta drift
Nargo.toml needs [package] name/type = "bin" and an empty [dependencies].

PROVE AND VERIFY (single-threaded — no COOP/COEP headers needed)
  const api = await bb.Barretenberg.new({ threads: 1 });
  const backend = new bb.UltraHonkBackend(program.bytecode, api);
  const { witness } = await noir.execute(inputs);
  const proof = await backend.generateProof(witness);
  const ok = await backend.verifyProof(proof);
threads > 1 needs SharedArrayBuffer and cross-origin isolation, which breaks
third-party embeds. Stay single-threaded.

UX BUDGET (mid-range mobile) — wrap EVERY phase in its own timeout with a
phase-specific message:
  load 90s (tens of MB, then HTTP-cached) · compile seconds · witness 30s ·
  prove 180s (dominates) · verify 60s
The libraries fetch their wasm internally, so fetch progress is unavailable: use a
PerformanceObserver on "resource" entries filtered to .wasm/.mjs and report
transferSize. Cache { program, noir, backend } in a ref; clear it on any failure so
Retry re-downloads instead of reusing a half-initialised backend. Offer "Retry" and
"Reload the prover", and say plainly that ad-blockers, offline mode and strict
proxies can block wasm.

HONESTY REQUIREMENT
Show the circuit source verbatim and include a tamper button that flips one byte of
proof.proof and re-verifies — verifyProof must return false. State what the circuit
does NOT prove (e.g. it does not check an issuer signature unless the circuit does).

MODE GOTCHAS
Both Vite rules are mandatory — a missing optimizeDeps.exclude hangs with no error, and a re-bundled noir_wasm breaks only in production. Keep noir_wasm and noir_js on the same beta. Never import the proving packages at module scope. Bound every phase with its own timeout; proving with one thread can exceed a minute on mobile.

PROVING PIPELINE (client-only component, inside the handler)
```ts
const { compile, createFileManager } = await import(/* @vite-ignore */ "/vendor/noir_wasm/main.mjs");
const { Noir } = await import("@noir-lang/noir_js");
const bb = await import("@aztec/bb.js");

// 1. compile
const fm = createFileManager("/");
await fm.writeFile("./src/main.nr", new Blob([CIRCUIT_SOURCE]).stream());
await fm.writeFile("./Nargo.toml", new Blob([NARGO_TOML]).stream());
const compiled = await compile(fm);
const program = "program" in compiled ? compiled.program : compiled;

// 2. backend (single-threaded — works on any origin)
const api = await bb.Barretenberg.new({ threads: 1 });
const backend = new bb.UltraHonkBackend(program.bytecode, api);

// 3. witness -> proof -> verify
const noir = new Noir(program);
const { witness } = await noir.execute(inputs);
const proof = await backend.generateProof(witness);   // { proof, publicInputs }
const ok = await backend.verifyProof(proof);          // true

// 4. tamper check — the only convincing evidence the proof is real
const bad = { ...proof, proof: (() => { const c = proof.proof.slice(); c[0] ^= 1; return c; })() };
const shouldBeFalse = await backend.verifyProof(bad);
```
NARGO_TOML = `[package]\nname = "app"\ntype = "bin"\n\n[dependencies]\n`

CIRCUIT — Merkle set membership (in the list, without saying which row)
```rust
// src/main.nr — depth 4 allowlist, 16 leaves
fn main(leaf: Field, path: [Field; 4], index: [u1; 4], root: pub Field) {
    let mut node = leaf;
    for i in 0..4 {
        let (l, r) = if index[i] == 0 { (node, path[i]) } else { (path[i], node) };
        node = std::hash::pedersen_hash([l, r]);
    }
    assert(node == root);
}
```
Build the tree in TypeScript with the SAME hash the circuit uses, publish only the
root, and keep the leaf and sibling path private. The verifier learns that the
holder is in the set — not which member they are, and not the set's contents.

USER FLOW (build exactly this, nothing more)
1. The app shows a small public allowlist (16 entries) and its Merkle root.
2. The user picks or pastes their own entry — that choice stays client-side.
3. Press "Prove membership" -> proof verifies against the public root only.
4. Show that the result reveals the root and the verdict, and nothing about the row.

DESIGN
Editorial, high-contrast, generous whitespace. One accent colour used sparingly.
Show the cryptography honestly: render the circuit source verbatim, the per-phase
timings, the public inputs and the raw proof in a collapsible panel so a judge can
see there is no trick. Truncate proofs, hashes and commitments in prose
(first 12 + last 6) with the full value behind a copy button. Proving can take a
minute on a phone — show phase-by-phase progress, never a bare spinner.
Mobile first — a judge will open this on a phone.

MARKET (for the pitch slide, not the UI)
TAM $5B — global dance industry · SAM $1.2B — classical ballet training · SOM $60M — professional ballet companies

GOTCHAS (universal — every one of these has burned a build)
- optimizeDeps.exclude is MANDATORY for all three proving packages. If esbuild
  pre-bundles them it rewrites the wasm-bindgen glue and wasm instantiation HANGS
  WITH NO ERROR. A step that never finishes and never throws is always this.
- noir_wasm's published browser bundle must be served VERBATIM. Re-bundling its
  vendored @ltd/j-toml emits `const Infinity = Infinity`, so importing the compiler
  throws "Cannot access 'Infinity' before initialization" (minified: "Cannot access
  'j' before initialization"). It breaks the PRODUCTION build only — preview looks
  fine and the published site dies.
- Pin @noir-lang/noir_wasm and @noir-lang/noir_js to the SAME beta, and @aztec/bb.js
  to a version that speaks their ACIR. A skew shows up as an opaque
  witness-generation failure on inputs you know are valid.
- `Barretenberg.new({ threads: 1 })`. More threads need SharedArrayBuffer, which
  needs COOP/COEP headers, which break third-party embeds.
- `compile()` returns `{ program }` on some betas and the program directly on
  others. Normalise: `const program = "program" in compiled ? compiled.program : compiled`.
- Nargo.toml is required in the virtual file manager: `[package] name/type = "bin"`
  plus an empty `[dependencies]`. Omitting it fails with a path error.
- Wrap EVERY phase in its own timeout with a phase-specific message
  (load 90s · compile 30s · witness 30s · prove 180s · verify 60s). An unbounded
  await is indistinguishable from the silent optimizer hang above.
- The libraries fetch their wasm internally, so `fetch` progress is unavailable.
  Use a PerformanceObserver on "resource" entries filtered to .wasm/.mjs and report
  `transferSize` if you want real byte counts.
- Cache `{ program, noir, backend }` in a ref and CLEAR it on failure, so Retry
  re-downloads instead of reusing a half-initialised backend. Say plainly that
  ad-blockers, offline mode and strict proxies can block wasm.
- Never print a raw proof, hash or commitment inline in prose: truncate
  (first 12 + last 6) with the full value behind a copy button or a JSON panel.
- Be honest about the trust boundary. Hashing a value and asserting a range proves
  knowledge of a preimage plus the range — it does NOT prove who issued the value
  unless the circuit verifies a signature. Say so in the UI.

REFERENCE MATERIAL (if you need more than the above)
- Noir docs: https://noir-lang.org/docs/
- Full machine-readable Noir brief: https://noir-lang.org/docs/llms-full.txt
- Noir repo: https://github.com/noir-lang/noir
- Barretenberg / bb.js: https://github.com/AztecProtocol/aztec-packages/tree/master/barretenberg
- Awesome Noir (examples, tooling): https://github.com/noir-lang/awesome-noir
- This catalog as one text file: https://identusprompts.lovable.app/llms-full.txt

DELIVERABLE
A working single-page demo where a real proof is generated and verified in the
browser end-to-end, plus a one-paragraph README naming the Noir primitive used,
what the circuit asserts, and what it deliberately does NOT prove.
Built for the Noir zero-knowledge catalyst — organised by StreetKode Fam during Indian Krump Festival 14.
Appendix · Market

Market sizing.

TAM
$5B
global dance industry
SAM
$1.2B
classical ballet training
SOM
$60M
professional ballet companies

Indicative figures for hackathon pitches — refine with your own research before raising.

See also

Adjacent entries.