📷 Photography · commercial

Sample Clearance Verification

Sample Clearance Verification verifies the issuer's signature inside the circuit so commercial can prove an attestation exists without showing it.

In-Circuit Signature· check the issuer, hide the document
Section · Noir

The primitive.

full primer →

Photographers prove an authority already signed off on commercial by checking that signature inside the circuit — the attestation itself stays on their device.

Why this primitiveIn-circuit signature verification is right for commercial in Photography because the claim only counts if a specific authority vouched for it, and the holder should be able to prove that vouching happened without handing over the document.

Primitive
verify an ECDSA (secp256k1/secp256r1) or EdDSA signature inside the circuit — `std::ecdsa_secp256k1::verify_signature(pub_key_x, pub_key_y, signature, message_hash)` — with the signed payload private and the issuer's public key public. The proof says 'a named authority signed something with these properties' without publishing the signed document itself
Drives the UI as
a verification step that trusts an issuer's attestation — a licence, a diploma, a payslip, a lab result — while the attestation and its contents stay with the holder
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 "Sample Clearance Verification" as a ONE-SHOT Lovable build.
Single-page TanStack Start app with a real Noir zero-knowledge proof. Cut scope ruthlessly.

CONCEPT
Verify that a sample has been cleared for use while keeping the details of the sample private.
Discipline: Photography (commercial).
Recipe: In-Circuit Signature (check the issuer, hide the document) as the single Noir primitive.
Why zero-knowledge: In-circuit signature verification is right for commercial in Photography because the claim only counts if a specific authority vouched for it, and the holder should be able to prove that vouching happened without handing over the document.

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 — In-circuit signature verification (trust the issuer, hide the document)
```rust
// src/main.nr
fn main(
    message_hash: [u8; 32],          // private: the attestation being proved
    signature: [u8; 64],             // private
    pub_key_x: pub [u8; 32],         // public: the issuer everyone trusts
    pub_key_y: pub [u8; 32],
) {
    let ok = std::ecdsa_secp256k1::verify_signature(pub_key_x, pub_key_y, signature, message_hash);
    assert(ok);
}
```
Sign the payload outside the circuit (any secp256k1 signer; `@noble/curves` in a
script is enough for the demo) and feed the raw bytes in. The proof says "this
named authority signed something with these properties" while the signed document
stays with the holder. State clearly that the circuit does not check WHAT was
signed unless you also assert over the payload.

USER FLOW (build exactly this, nothing more)
1. The app ships a demo issuer keypair and a signed attestation (both visible).
2. The user presses "Prove" -> the circuit verifies the signature privately.
3. Show the public issuer key, the PASS gate, and the truncated proof.
4. A "forge" button corrupts the signature and shows witness generation refusing.

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 $4.0B — global wedding services and photography software · SAM $1.2B — wedding photojournalism and editing tools · SOM $60M — high-end wedding photographers and studio owners

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
$4.0B
global wedding services and photography software
SAM
$1.2B
wedding photojournalism and editing tools
SOM
$60M
high-end wedding photographers and studio owners

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

See also

Adjacent entries.