Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/web-starter-playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ jobs:
run: |
yarn test:e2e

- name: Install JS dependencies (next)
working-directory: web-starter/web/nextjs
run: |
yarn install

- name: Install Playwright Browsers (next)
working-directory: web-starter/web/nextjs
run: |
yarn playwright install --with-deps

- name: Run Playwright tests (next)
working-directory: web-starter/web/nextjs
run: |
yarn test:e2e

- name: Create issue on failure (nightly)
if: failure() && github.event_name == 'schedule'
uses: actions/github-script@v6
Expand Down
7 changes: 7 additions & 0 deletions web-starter/web/nextjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules/
.next/
dist/
.env*
yarn.lock
npm-debug.log*
.DS_Store
19 changes: 19 additions & 0 deletions web-starter/web/nextjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Introduction

A simple Noir circuit with browser proving with bb.js
This is a Next.js version, similar to the vite and webpack examples.

Tested with Noir 1.0.0-beta.6, bb 0.84.0, and Next.js 14.

## Setup

```bash
(cd ../../circuits && ./build.sh)
yarn
```

## Run

```bash
yarn dev
```
45 changes: 45 additions & 0 deletions web-starter/web/nextjs/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
body {
font-family: Arial, sans-serif;
background: #f8f8f8;
margin: 0;
padding: 0;
}

.container, main {
max-width: 600px;
margin: 40px auto;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
padding: 32px;
}

h1 {
font-size: 2rem;
margin-bottom: 1.5rem;
}

button {
padding: 0.5rem 1.5rem;
font-size: 1rem;
border-radius: 4px;
border: none;
background: #0070f3;
color: #fff;
cursor: pointer;
margin-bottom: 1rem;
}

button:disabled {
background: #aaa;
cursor: not-allowed;
}

#result {
background: #f4f4f4;
border-radius: 4px;
padding: 1rem;
min-height: 2rem;
margin-top: 1rem;
font-family: monospace;
}
12 changes: 12 additions & 0 deletions web-starter/web/nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import './globals.css';
import type { ReactNode } from 'react';

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
{children}
</body>
</html>
);
}
10 changes: 10 additions & 0 deletions web-starter/web/nextjs/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import ProofComponent from './proof-component';

export default function HomePage() {
return (
<main className="container">
<h1>Noir UH Starter (Next.js)</h1>
<ProofComponent />
</main>
);
}
54 changes: 54 additions & 0 deletions web-starter/web/nextjs/app/proof-component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use client";
import { useState } from "react";
import { UltraHonkBackend } from "@aztec/bb.js";
import circuit from "../../../circuits/target/noir_uh_starter.json";
import { Noir } from "@noir-lang/noir_js";

export default function ProofComponent() {
const [result, setResult] = useState("");
const [loading, setLoading] = useState(false);

async function generateProof() {
setLoading(true);
setResult((prev) => prev + "Generating proof...\n\n");
try {
const noir = new Noir(circuit as any);
const honk = new UltraHonkBackend((circuit as any).bytecode, {
threads: 8, // This will only work if SharedArrayBuffer is enabled (see next.config.mjs)
});
const inputs = { x: 3, y: 3 };
const { witness } = await noir.execute(inputs);
const { proof, publicInputs } = await honk.generateProof(witness);
setResult((prev) => prev + "Proof: " + proof + "\n\n");
setResult((prev) => prev + "Public inputs: " + publicInputs + "\n\n");
const verified = await honk.verifyProof({ proof, publicInputs });
setResult((prev) => prev + "Verified: " + verified + "\n\n");

// Send proof to server for server-side verification
setResult((prev) => prev + "Verifying on server...\n\n");
const response = await fetch("/api/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ proof, publicInputs }),
});
const serverResult = await response.json();
setResult(
(prev) => prev + "Server verified: " + serverResult.verified + "\n\n"
);
} catch (error) {
setResult((prev) => prev + "Error: " + error + "\n\n");
}
setLoading(false);
}

return (
<div>
<button id="generateProofBtn" onClick={generateProof} disabled={loading}>
{loading ? "Generating..." : "Generate Proof"}
</button>
<div style={{ whiteSpace: "pre-wrap" }} id="result">
{result}
</div>
</div>
);
}
6 changes: 6 additions & 0 deletions web-starter/web/nextjs/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
32 changes: 32 additions & 0 deletions web-starter/web/nextjs/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config) => {
config.experiments = {
// This is required for @aztec/bb.js as it imports wasm files
asyncWebAssembly: true,
layers: true,
};
return config;
},
// These headers enable SharedArrayBuffer which is required for running
// @aztec/bb.js wasm in multiple threads.
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "Cross-Origin-Embedder-Policy",
value: "require-corp",
},
{
key: "Cross-Origin-Opener-Policy",
value: "same-origin",
},
],
},
];
},
};

export default nextConfig;
25 changes: 25 additions & 0 deletions web-starter/web/nextjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "nextjs-noir",
"type": "module",
"dependencies": {
"@aztec/bb.js": "0.84.0",
"@noir-lang/noir_js": "1.0.0-beta.6",
"next": "^15.3.4",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@playwright/test": "^1.53.0",
"@types/node": "^22.10.1",
"@types/react": "^18.2.62",
"@types/react-dom": "^18.2.19",
"typescript": "^5.8.3"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"test:e2e": "playwright test"
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
83 changes: 83 additions & 0 deletions web-starter/web/nextjs/pages/api/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { UltraHonkBackend, BarretenbergVerifier } from '@aztec/bb.js';
import { promises as fs } from 'fs';
import path from 'path';

// Load circuit data at module level (outside the handler)
let circuit: any = null;
let honk: UltraHonkBackend | null = null;

async function initializeCircuit() {
if (!circuit) {
const circuitPath = path.resolve(process.cwd(), '../../circuits/target/noir_uh_starter.json');
const circuitData = await fs.readFile(circuitPath, 'utf-8');
circuit = JSON.parse(circuitData);
honk = new UltraHonkBackend(circuit.bytecode, {
threads: 8,

// By default, bb.js downloads CRS files to ~/.bb-crs. For serverless environments where
// this path isn't writable, configure an alternate path (e.g. /tmp) using crsPath option
// crsPath: `/tmp`
});
}
}

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
res.setHeader('Allow', ['POST']);
return res.status(405).end(`Method ${req.method} Not Allowed`);
}

try {
// Initialize circuit if not already done
await initializeCircuit();

const { proof, publicInputs } = req.body;

if (!proof || !publicInputs) {
return res.status(400).json({
error: 'Missing proof or publicInputs',
verified: false
});
}

if (!honk) {
throw new Error('Backend not initialized');
}

// Convert proof to Uint8Array if it's not already
let proofArray: Uint8Array;
if (proof instanceof Uint8Array) {
proofArray = proof;
} else if (Array.isArray(proof)) {
proofArray = new Uint8Array(proof);
} else if (typeof proof === 'object' && proof !== null) {
// Handle serialized Uint8Array (object with numeric keys)
const values = Object.values(proof) as number[];
proofArray = new Uint8Array(values);
} else {
throw new Error('Invalid proof format');
}

const publicInputsArray = Array.isArray(publicInputs) ? publicInputs : [publicInputs];

const verified = await honk.verifyProof({
proof: proofArray,
publicInputs: publicInputsArray
});

return res.status(200).json({
verified,
message: verified ? 'Proof verified successfully' : 'Proof verification failed'
});
} catch (error) {
console.error('Server-side verification error:', error);
return res.status(500).json({
error: String(error),
verified: false
});
}
}
22 changes: 22 additions & 0 deletions web-starter/web/nextjs/playwright.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// playwright.config.js
// @ts-check
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
webServer: {
command: 'yarn dev',
port: 3000,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
},
use: {
baseURL: 'http://localhost:3000',
headless: true,
},
projects: [
{ name: 'chromium', use: { browserName: 'chromium' } },
{ name: 'firefox', use: { browserName: 'firefox' } },
{ name: 'webkit', use: { browserName: 'webkit' } },
],
};

module.exports = config;
Loading