From a3918e07a1f9683b0498626357860b83c2df24c7 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:00:32 +0000 Subject: [PATCH 1/5] feat: circuits release --- .github/workflows/releases.yml | 59 +++++ .gitignore | 2 + package.json | 2 + scripts/README.md | 87 ++++++++ scripts/build-circuits.ts | 391 +++++++++++++++++++++++++++++++++ scripts/circuit-artifacts.ts | 73 ++++++ 6 files changed, 614 insertions(+) create mode 100644 scripts/build-circuits.ts create mode 100644 scripts/circuit-artifacts.ts diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 5a1842f5aa..41e5dbfede 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -310,6 +310,55 @@ jobs: working-directory: packages/enclave-react run: npm publish --access public --tag ${{ steps.npm_tag.outputs.tag }} --provenance + download-circuits: + name: Download Circuit Artifacts + runs-on: ubuntu-latest + needs: validate-and-prepare + outputs: + found: ${{ steps.pull.outputs.found }} + source_hash: ${{ steps.pull.outputs.source_hash }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - uses: pnpm/action-setup@v4 + + - run: pnpm install --frozen-lockfile + + - name: Pull circuit artifacts + id: pull + run: | + SOURCE_HASH=$(pnpm tsx scripts/build-circuits.ts hash) + echo "source_hash=$SOURCE_HASH" >> $GITHUB_OUTPUT + + if git fetch origin circuit-artifacts 2>/dev/null; then + mkdir -p dist/circuits + git archive origin/circuit-artifacts | tar -x -C dist/circuits + echo "found=true" >> $GITHUB_OUTPUT + else + echo "found=false" >> $GITHUB_OUTPUT + fi + + - name: Create release archive + if: steps.pull.outputs.found == 'true' + run: | + cd dist + tar -czvf circuits-${{ needs.validate-and-prepare.outputs.version }}.tar.gz circuits/ + + - uses: actions/upload-artifact@v4 + if: steps.pull.outputs.found == 'true' + with: + name: noir-circuits + path: | + dist/circuits-*.tar.gz + dist/circuits/SHA256SUMS + dist/circuits/checksums.json + create-github-release: name: Create GitHub Release runs-on: ubuntu-latest @@ -321,6 +370,7 @@ jobs: build-binaries, publish-rust-crates, publish-npm-packages, + download-circuits, ] if: always() && needs.validate-and-prepare.result == 'success' && needs.build-binaries.result == 'success' steps: @@ -456,6 +506,15 @@ jobs: echo '```' >> release_notes.md + if [[ "${{ needs.download-circuits.outputs.found }}" == "true" ]]; then + cat >> release_notes.md << EOF + + ## šŸ”® Noir Circuits + + Source hash: \`${{ needs.download-circuits.outputs.source_hash }}\` + EOF + fi + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: diff --git a/.gitignore b/.gitignore index 4c704e1227..e25779c550 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ node_modules target *.DS_Store + +dist \ No newline at end of file diff --git a/package.json b/package.json index dd722f7b56..9b2ecbee1e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ }, "scripts": { "bump:versions": "tsx scripts/bump-versions.ts", + "build:circuits": "tsx scripts/build-circuits.ts", + "store:circuits": "tsx scripts/circuit-artifacts.ts", "clean": "tsx scripts/clean.ts", "compile": "pnpm build:ts && pnpm rust:build", "lint": "eslint . && pnpm evm:lint && pnpm rust:lint && pnpm noir:lint", diff --git a/scripts/README.md b/scripts/README.md index 65cb6ac3d2..db7b793476 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -157,3 +157,90 @@ pnpm clean --help - **Provides granular control** over what gets cleaned via skip options - **Shows detailed statistics** about what was removed and space freed - **Prevents accidental deletion** of important files by using a whitelist approach + +## Circuit Builder + +`build-circuits.ts` - Compiles Noir circuits, generates verification keys, and prepares release +artifacts. + +### Usage + +```bash +# Build all circuits +pnpm build:circuits + +# Build only insecure circuits (skip heavy production ones) +pnpm build:circuits --skip-production + +# Skip verification key generation (faster) +pnpm build:circuits --skip-vk + +# Dry run to see what would be built +pnpm build:circuits --dry-run + +# Get source hash for change detection +pnpm build:circuits hash + +# Generate manifest file +pnpm build:circuits manifest --version 1.0.0 +``` + +### What it does + +1. **Discovers circuits** in `circuits/bin/insecure/` and `circuits/bin/production/` +2. **Compiles** each circuit using `nargo compile` +3. **Generates verification keys** using `bb write_vk` +4. **Sanitizes paths** in compiled JSON (removes local filesystem paths for opsec) +5. **Generates checksums** (`SHA256SUMS` and `checksums.json`) +6. **Copies artifacts** to `dist/circuits/` + +### Options + +- `--skip-production` - Only build insecure circuits +- `--skip-vk` - Skip verification key generation +- `--skip-checksums` - Skip checksum generation +- `--env ` - Environments (comma-separated: insecure,production) +- `--circuit ` - Build specific circuit(s) +- `-o, --output ` - Output directory (default: dist/circuits) +- `--dry-run` - Show what would be built +- `--no-clean` - Don't clean output directory + +### Prerequisites + +- `nargo` - Noir compiler ([install](https://noir-lang.org/docs/getting_started/installation/)) +- `bb` - Barretenberg prover (for verification keys) + +## Circuit Artifacts + +`circuit-artifacts.ts` - Push/pull pre-built circuit artifacts via git branch. + +### Usage + +```bash +# Build circuits locally, then push to git branch +pnpm build:circuits --skip-production +pnpm store:circuits push + +# Pull circuits from git branch (used by CI) +pnpm store:circuits pull +``` + +### What it does + +- **Push**: Copies `dist/circuits/` to the `circuit-artifacts` orphan branch and pushes to origin +- **Pull**: Fetches the `circuit-artifacts` branch and extracts to `dist/circuits/` + +### Workflow + +Since production circuits are heavy to compile, they're built locally and stored in a git branch: + +1. **Local**: Build circuits and push to branch + + ```bash + pnpm build:circuits --skip-production + pnpm tsx scripts/circuit-artifacts.ts push + ``` + +2. **CI**: Pulls from branch during release, attaches to GitHub release + +3. **After release**: Circuits live permanently in release assets diff --git a/scripts/build-circuits.ts b/scripts/build-circuits.ts new file mode 100644 index 0000000000..7f8751d4bc --- /dev/null +++ b/scripts/build-circuits.ts @@ -0,0 +1,391 @@ +#!/usr/bin/env tsx +// SPDX-License-Identifier: LGPL-3.0-only +// Noir Circuit Builder for Enclave Monorepo + +import { execSync } from 'child_process' +import { createHash } from 'crypto' +import { appendFileSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs' +import { basename, join, resolve } from 'path' + +// Types & Constants +const ENVIRONMENTS = { INSECURE: 'insecure', PRODUCTION: 'production' } as const +type Environment = (typeof ENVIRONMENTS)[keyof typeof ENVIRONMENTS] +const ALL_ENVIRONMENTS: Environment[] = [ENVIRONMENTS.INSECURE, ENVIRONMENTS.PRODUCTION] + +interface CircuitInfo { + name: string + environment: Environment + path: string +} +interface CompiledCircuit { + name: string + environment: Environment + artifacts: { json?: string; vk?: string } + checksums: { json?: string; vk?: string } +} +interface BuildOptions { + environments?: Environment[] + circuits?: string[] + skipChecksums?: boolean + skipVk?: boolean + outputDir?: string + clean?: boolean + dryRun?: boolean + verbose?: boolean +} +interface BuildResult { + success: boolean + compiled: CompiledCircuit[] + checksumFile?: string + releaseDir?: string + errors: string[] + sourceHash?: string +} + +class NoirCircuitBuilder { + private rootDir: string + private circuitsDir: string + private options: BuildOptions + + constructor(rootDir?: string, options: BuildOptions = {}) { + this.rootDir = rootDir ?? resolve(__dirname, '..') + this.circuitsDir = join(this.rootDir, 'circuits', 'bin') + this.options = { + environments: ALL_ENVIRONMENTS, + outputDir: join(this.rootDir, 'dist', 'circuits'), + clean: true, + skipVk: false, + ...options, + } + } + + async buildAll(): Promise { + const result: BuildResult = { success: true, compiled: [], errors: [] } + + console.log('šŸ”® Building Noir circuits...') + + try { + this.checkTool('nargo --version', 'nargo') + if (!this.options.skipVk) this.checkTool('bb --version', 'bb') + + const circuits = this.discoverCircuits() + if (circuits.length === 0) { + console.log(' āš ļø No circuits found') + return result + } + + console.log(` Found ${circuits.length} circuit(s)`) + + if (this.options.dryRun) { + console.log('\n Would build:', circuits.map((c) => `${c.environment}/${c.name}`).join(', ')) + return result + } + + if (this.options.clean && existsSync(this.options.outputDir!)) { + rmSync(this.options.outputDir!, { recursive: true }) + } + mkdirSync(this.options.outputDir!, { recursive: true }) + + result.sourceHash = this.computeSourceHash() + + for (const circuit of circuits) { + try { + result.compiled.push(this.buildCircuit(circuit)) + } catch (error: any) { + result.errors.push(`${circuit.name}: ${error.message}`) + result.success = false + } + } + + if (!this.options.skipChecksums && result.compiled.length > 0) { + result.checksumFile = this.generateChecksumFile(result.compiled) + } + + result.releaseDir = this.copyArtifacts(result.compiled) + console.log(`\nāœ… Built ${result.compiled.length} circuits`) + } catch (error: any) { + result.success = false + result.errors.push(error.message) + console.error('āŒ Error:', error.message) + } + + return result + } + + private checkTool(cmd: string, name: string): void { + try { + execSync(cmd, { stdio: ['pipe', 'pipe', 'pipe'] }) + } catch { + throw new Error(`${name} is not installed or not in PATH`) + } + } + + private discoverCircuits(): CircuitInfo[] { + const circuits: CircuitInfo[] = [] + if (!existsSync(this.circuitsDir)) return circuits + + for (const env of this.options.environments ?? ALL_ENVIRONMENTS) { + const envDir = join(this.circuitsDir, env) + if (!existsSync(envDir)) continue + + for (const entry of readdirSync(envDir)) { + const circuitPath = join(envDir, entry) + if (statSync(circuitPath).isDirectory() && existsSync(join(circuitPath, 'Nargo.toml'))) { + if (!this.options.circuits || this.options.circuits.includes(entry)) { + circuits.push({ name: entry, environment: env, path: circuitPath }) + } + } + } + } + return circuits + } + + private buildCircuit(circuit: CircuitInfo): CompiledCircuit { + const packageName = this.getPackageName(circuit.path) + const result: CompiledCircuit = { + name: circuit.name, + environment: circuit.environment, + artifacts: {}, + checksums: {}, + } + + execSync('nargo compile', { cwd: circuit.path, stdio: 'pipe' }) + + const envDir = join(this.circuitsDir, circuit.environment) + const targetDirs = [join(envDir, 'target'), join(this.circuitsDir, 'target'), join(circuit.path, 'target')] + + let jsonFile: string | null = null + let targetDir: string | null = null + + for (const dir of targetDirs) { + if (!existsSync(dir)) continue + const candidate = join(dir, `${packageName}.json`) + if (existsSync(candidate)) { + jsonFile = candidate + targetDir = dir + break + } + } + + if (jsonFile && targetDir) { + this.sanitizePaths(jsonFile) + result.artifacts.json = jsonFile + result.checksums.json = this.checksum(jsonFile) + + if (!this.options.skipVk) { + const vkFile = this.generateVk(jsonFile, targetDir, packageName) + if (vkFile) { + result.artifacts.vk = vkFile + result.checksums.vk = this.checksum(vkFile) + } + } + console.log(` āœ“ ${circuit.environment}/${circuit.name}`) + } else { + console.log(` āš ļø ${circuit.environment}/${circuit.name}: no artifact found`) + } + + return result + } + + private generateVk(jsonFile: string, targetDir: string, packageName: string): string | null { + const vkFile = join(targetDir, `${packageName}.vk`) + try { + execSync(`bb write_vk -b "${jsonFile}" -o "${targetDir}"`, { stdio: 'pipe' }) + const defaultVk = join(targetDir, 'vk') + if (existsSync(defaultVk)) { + if (existsSync(vkFile)) rmSync(vkFile) + copyFileSync(defaultVk, vkFile) + rmSync(defaultVk) + } + return existsSync(vkFile) ? vkFile : null + } catch { + return null + } + } + + private getPackageName(circuitPath: string): string { + try { + const content = readFileSync(join(circuitPath, 'Nargo.toml'), 'utf-8') + const match = content.match(/^name\s*=\s*"([^"]+)"/m) + if (match) return match[1] + } catch { + // Ignore errors + } + return basename(circuitPath) + } + + private sanitizePaths(jsonFile: string): void { + try { + const content = readFileSync(jsonFile, 'utf-8') + const sanitized = content + .replace(/"path"\s*:\s*"[^"]*[/\\](enclave[/\\]circuits[/\\][^"]+)"/g, '"path":"$1"') + .replace(/"path"\s*:\s*"(?:\/[^"]*|[A-Za-z]:\\[^"]*)[/\\](circuits[/\\][^"]+)"/g, '"path":"enclave/$1"') + if (content !== sanitized) writeFileSync(jsonFile, sanitized) + } catch { + // Ignore errors + } + } + + private checksum(filePath: string): string { + return createHash('sha256').update(readFileSync(filePath)).digest('hex') + } + + private generateChecksumFile(compiled: CompiledCircuit[]): string { + const lines: string[] = [] + const checksums: Record = {} + + for (const c of compiled) { + const prefix = `${c.environment}/${c.name}` + if (c.checksums.json && c.artifacts.json) { + const f = `${prefix}/${basename(c.artifacts.json)}` + checksums[f] = c.checksums.json + lines.push(`${c.checksums.json} ${f}`) + } + if (c.checksums.vk && c.artifacts.vk) { + const f = `${prefix}/${basename(c.artifacts.vk)}` + checksums[f] = c.checksums.vk + lines.push(`${c.checksums.vk} ${f}`) + } + } + + const outputDir = this.options.outputDir! + writeFileSync(join(outputDir, 'SHA256SUMS'), lines.join('\n') + '\n') + writeFileSync( + join(outputDir, 'checksums.json'), + JSON.stringify({ algorithm: 'sha256', generated: new Date().toISOString(), files: checksums }, null, 2) + '\n', + ) + return join(outputDir, 'SHA256SUMS') + } + + private copyArtifacts(compiled: CompiledCircuit[]): string { + const outputDir = this.options.outputDir! + for (const c of compiled) { + if (!c.artifacts.json && !c.artifacts.vk) continue + const dir = join(outputDir, c.environment, c.name) + mkdirSync(dir, { recursive: true }) + if (c.artifacts.json) copyFileSync(c.artifacts.json, join(dir, basename(c.artifacts.json))) + if (c.artifacts.vk) copyFileSync(c.artifacts.vk, join(dir, basename(c.artifacts.vk))) + } + return outputDir + } + + computeSourceHash(): string { + const hash = createHash('sha256') + const circuits = this.discoverCircuits().sort((a, b) => `${a.environment}/${a.name}`.localeCompare(`${b.environment}/${b.name}`)) + for (const c of circuits) this.hashDir(c.path, hash) + return hash.digest('hex').substring(0, 16) + } + + private hashDir(dirPath: string, hash: ReturnType): void { + for (const entry of readdirSync(dirPath).sort()) { + if (entry === 'target' || entry.startsWith('.')) continue + const fullPath = join(dirPath, entry) + const stat = statSync(fullPath) + if (stat.isDirectory()) this.hashDir(fullPath, hash) + else if (stat.isFile()) { + hash.update(entry) + hash.update(readFileSync(fullPath)) + } + } + } + + writeGitHubOutput(result: BuildResult): void { + const output = process.env.GITHUB_OUTPUT + const lines = [ + `circuits_built=${result.compiled.length}`, + `circuits_success=${result.success}`, + `source_hash=${result.sourceHash ?? 'unknown'}`, + result.releaseDir ? `artifacts_dir=${result.releaseDir}` : '', + ].filter(Boolean) + + if (output) appendFileSync(output, lines.join('\n') + '\n') + else console.log('\nšŸ“‹ CI Output:', lines.join(', ')) + } + + generateManifest(version: string): string { + const outputDir = this.options.outputDir! + mkdirSync(outputDir, { recursive: true }) + const manifest = { + version, + generatedAt: new Date().toISOString(), + sourceHash: this.computeSourceHash(), + circuits: this.discoverCircuits().map((c) => ({ + name: c.name, + environment: c.environment, + artifacts: { json: `${c.environment}/${c.name}/${c.name}.json`, vk: `${c.environment}/${c.name}/${c.name}.vk` }, + })), + } + const path = join(outputDir, 'manifest.json') + writeFileSync(path, JSON.stringify(manifest, null, 2) + '\n') + return path + } +} + +// CLI +async function main() { + const args = process.argv.slice(2) + const options: BuildOptions = {} + let command = 'build' + let version: string | undefined + + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg === '-h' || arg === '--help') { + showHelp() + process.exit(0) + } else if (arg === '--dry-run') options.dryRun = true + else if (arg === '-v' || arg === '--verbose') options.verbose = true + else if (arg === '--skip-checksums') options.skipChecksums = true + else if (arg === '--skip-vk') options.skipVk = true + else if (arg === '--skip-production') options.environments = [ENVIRONMENTS.INSECURE] + else if (arg === '--no-clean') options.clean = false + else if (arg === '--env') options.environments = args[++i]?.split(',') as Environment[] + else if (arg === '--circuit') (options.circuits ??= []).push(args[++i]) + else if (arg === '-o' || arg === '--output') options.outputDir = resolve(args[++i]) + else if (arg === '--version') version = args[++i] + else if (['hash', 'manifest', 'build'].includes(arg)) command = arg + } + + const builder = new NoirCircuitBuilder(undefined, options) + + if (command === 'hash') { + const hash = builder.computeSourceHash() + console.log(hash) + if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, `source_hash=${hash}\n`) + } else if (command === 'manifest') { + if (!version) { + console.error('āŒ --version required') + process.exit(1) + } + console.log(`āœ… Manifest: ${builder.generateManifest(version)}`) + } else { + const result = await builder.buildAll() + builder.writeGitHubOutput(result) + process.exit(result.success ? 0 : 1) + } +} + +function showHelp() { + console.log(` +Usage: build-circuits [command] [options] + +Commands: build (default), hash, manifest + +Options: + --skip-production Only build insecure circuits + --skip-vk Skip verification key generation + --skip-checksums Skip checksum generation + --env Environments (comma-separated: insecure,production) + --circuit Build specific circuit(s) + -o, --output Output directory (default: dist/circuits) + --version Version for manifest + --dry-run Show what would be built + --no-clean Don't clean output directory + -v, --verbose Verbose output + -h, --help Show help +`) +} + +if (require.main === module) main() + +export { NoirCircuitBuilder, BuildOptions, BuildResult, CompiledCircuit, CircuitInfo, Environment, ENVIRONMENTS } diff --git a/scripts/circuit-artifacts.ts b/scripts/circuit-artifacts.ts new file mode 100644 index 0000000000..45bb73bfda --- /dev/null +++ b/scripts/circuit-artifacts.ts @@ -0,0 +1,73 @@ +#!/usr/bin/env tsx +// SPDX-License-Identifier: LGPL-3.0-only +// Push/pull circuit artifacts via git branch + +import { execSync } from 'child_process' +import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'fs' +import { join, resolve } from 'path' + +const BRANCH = 'circuit-artifacts' +const ROOT = resolve(__dirname, '..') +const DIST = join(ROOT, 'dist', 'circuits') + +const run = (cmd: string, cwd = ROOT) => execSync(cmd, { encoding: 'utf-8', cwd, stdio: 'pipe' }).trim() +const runV = (cmd: string, cwd = ROOT) => execSync(cmd, { cwd, stdio: 'inherit' }) + +async function push() { + if (!existsSync(DIST)) { + console.error('āŒ No artifacts. Run: pnpm build:circuits --skip-production') + process.exit(1) + } + + const hash = run('pnpm tsx scripts/build-circuits.ts hash') + const remote = run('git remote get-url origin') + const tmp = join(ROOT, '.tmp-circuits') + + if (existsSync(tmp)) rmSync(tmp, { recursive: true }) + + const branchExists = run(`git ls-remote --heads origin ${BRANCH}`).includes(BRANCH) + + if (branchExists) { + runV(`git clone --depth 1 --branch ${BRANCH} --single-branch ${remote} ${tmp}`) + for (const f of readdirSync(tmp)) if (f !== '.git') rmSync(join(tmp, f), { recursive: true }) + } else { + mkdirSync(tmp) + run('git init', tmp) + run(`git remote add origin ${remote}`, tmp) + run(`git checkout -b ${BRANCH}`, tmp) + } + + cpSync(DIST, tmp, { recursive: true }) + writeFileSync(join(tmp, 'SOURCE_HASH'), hash) + + run('git add -A', tmp) + try { + run(`git commit -m "circuits: ${hash}"`, tmp) + runV(`git push origin ${BRANCH} --force`, tmp) + console.log(`āœ… Pushed (${hash})`) + } catch { + console.log('āœ… No changes') + } + + rmSync(tmp, { recursive: true }) +} + +async function pull() { + try { + run(`git fetch origin ${BRANCH}`) + } catch { + console.error(`āŒ Branch '${BRANCH}' not found`) + process.exit(1) + } + + if (existsSync(DIST)) rmSync(DIST, { recursive: true }) + mkdirSync(DIST, { recursive: true }) + + runV(`git archive origin/${BRANCH} | tar -x -C ${DIST}`) + console.log(`āœ… Pulled to ${DIST}`) +} + +const cmd = process.argv[2] +if (cmd === 'push') push() +else if (cmd === 'pull') pull() +else console.log('Usage: circuit-artifacts.ts [push|pull]') From 42ee02a37859a5a470fe0987f02ef0d8a3e53bc5 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:04:31 +0000 Subject: [PATCH 2/5] chore: pr comments --- scripts/build-circuits.ts | 5 ++++- scripts/circuit-artifacts.ts | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/scripts/build-circuits.ts b/scripts/build-circuits.ts index 7f8751d4bc..ecc113c8fd 100644 --- a/scripts/build-circuits.ts +++ b/scripts/build-circuits.ts @@ -1,6 +1,9 @@ #!/usr/bin/env tsx // SPDX-License-Identifier: LGPL-3.0-only -// Noir Circuit Builder for Enclave Monorepo +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. import { execSync } from 'child_process' import { createHash } from 'crypto' diff --git a/scripts/circuit-artifacts.ts b/scripts/circuit-artifacts.ts index 45bb73bfda..19e2ec3ae1 100644 --- a/scripts/circuit-artifacts.ts +++ b/scripts/circuit-artifacts.ts @@ -1,6 +1,9 @@ #!/usr/bin/env tsx // SPDX-License-Identifier: LGPL-3.0-only -// Push/pull circuit artifacts via git branch +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. import { execSync } from 'child_process' import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'fs' @@ -55,15 +58,21 @@ async function push() { async function pull() { try { run(`git fetch origin ${BRANCH}`) - } catch { - console.error(`āŒ Branch '${BRANCH}' not found`) + } catch (e: any) { + const isNetworkError = + e.message?.includes('Could not resolve host') || e.message?.includes('unable to access') || e.message?.includes('Connection refused') + if (isNetworkError) { + console.error('āŒ Network error fetching branch') + } else { + console.error(`āŒ Branch '${BRANCH}' not found`) + } process.exit(1) } if (existsSync(DIST)) rmSync(DIST, { recursive: true }) mkdirSync(DIST, { recursive: true }) - runV(`git archive origin/${BRANCH} | tar -x -C ${DIST}`) + runV(`git archive origin/${BRANCH} | tar -x -C "${DIST}"`) console.log(`āœ… Pulled to ${DIST}`) } From 55eedc13603ba1e401bbe315685a075254371d90 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:46:41 +0000 Subject: [PATCH 3/5] chore: update to latest circuit structure changes --- scripts/README.md | 24 +++---- scripts/build-circuits.ts | 126 ++++++++++++++--------------------- scripts/circuit-artifacts.ts | 6 +- 3 files changed, 64 insertions(+), 92 deletions(-) diff --git a/scripts/README.md b/scripts/README.md index db7b793476..00d9c34aac 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -169,8 +169,8 @@ artifacts. # Build all circuits pnpm build:circuits -# Build only insecure circuits (skip heavy production ones) -pnpm build:circuits --skip-production +# Build only specific group (dkg or threshold) +pnpm build:circuits --group dkg # Skip verification key generation (faster) pnpm build:circuits --skip-vk @@ -180,14 +180,11 @@ pnpm build:circuits --dry-run # Get source hash for change detection pnpm build:circuits hash - -# Generate manifest file -pnpm build:circuits manifest --version 1.0.0 ``` ### What it does -1. **Discovers circuits** in `circuits/bin/insecure/` and `circuits/bin/production/` +1. **Discovers circuits** in `circuits/bin/dkg/` and `circuits/bin/threshold/` 2. **Compiles** each circuit using `nargo compile` 3. **Generates verification keys** using `bb write_vk` 4. **Sanitizes paths** in compiled JSON (removes local filesystem paths for opsec) @@ -196,11 +193,10 @@ pnpm build:circuits manifest --version 1.0.0 ### Options -- `--skip-production` - Only build insecure circuits +- `--group ` - Circuit groups (comma-separated: dkg,threshold) +- `--circuit ` - Build specific circuit(s) - `--skip-vk` - Skip verification key generation - `--skip-checksums` - Skip checksum generation -- `--env ` - Environments (comma-separated: insecure,production) -- `--circuit ` - Build specific circuit(s) - `-o, --output ` - Output directory (default: dist/circuits) - `--dry-run` - Show what would be built - `--no-clean` - Don't clean output directory @@ -218,7 +214,7 @@ pnpm build:circuits manifest --version 1.0.0 ```bash # Build circuits locally, then push to git branch -pnpm build:circuits --skip-production +pnpm build:circuits pnpm store:circuits push # Pull circuits from git branch (used by CI) @@ -232,14 +228,14 @@ pnpm store:circuits pull ### Workflow -Since production circuits are heavy to compile, they're built locally and stored in a git branch: +Circuits are built locally and stored in a git branch: 1. **Local**: Build circuits and push to branch - ```bash - pnpm build:circuits --skip-production +```bash + pnpm build:circuits pnpm tsx scripts/circuit-artifacts.ts push - ``` +``` 2. **CI**: Pulls from branch during release, attaches to GitHub release diff --git a/scripts/build-circuits.ts b/scripts/build-circuits.ts index ecc113c8fd..b42fef12b3 100644 --- a/scripts/build-circuits.ts +++ b/scripts/build-circuits.ts @@ -11,30 +11,29 @@ import { appendFileSync, copyFileSync, existsSync, mkdirSync, readdirSync, readF import { basename, join, resolve } from 'path' // Types & Constants -const ENVIRONMENTS = { INSECURE: 'insecure', PRODUCTION: 'production' } as const -type Environment = (typeof ENVIRONMENTS)[keyof typeof ENVIRONMENTS] -const ALL_ENVIRONMENTS: Environment[] = [ENVIRONMENTS.INSECURE, ENVIRONMENTS.PRODUCTION] +const CIRCUIT_GROUPS = { DKG: 'dkg', THRESHOLD: 'threshold' } as const +type CircuitGroup = (typeof CIRCUIT_GROUPS)[keyof typeof CIRCUIT_GROUPS] +const ALL_GROUPS: CircuitGroup[] = [CIRCUIT_GROUPS.DKG, CIRCUIT_GROUPS.THRESHOLD] interface CircuitInfo { name: string - environment: Environment + group: CircuitGroup path: string } interface CompiledCircuit { name: string - environment: Environment + group: CircuitGroup artifacts: { json?: string; vk?: string } checksums: { json?: string; vk?: string } } interface BuildOptions { - environments?: Environment[] + groups?: CircuitGroup[] circuits?: string[] skipChecksums?: boolean skipVk?: boolean outputDir?: string clean?: boolean dryRun?: boolean - verbose?: boolean } interface BuildResult { success: boolean @@ -54,7 +53,7 @@ class NoirCircuitBuilder { this.rootDir = rootDir ?? resolve(__dirname, '..') this.circuitsDir = join(this.rootDir, 'circuits', 'bin') this.options = { - environments: ALL_ENVIRONMENTS, + groups: ALL_GROUPS, outputDir: join(this.rootDir, 'dist', 'circuits'), clean: true, skipVk: false, @@ -80,7 +79,7 @@ class NoirCircuitBuilder { console.log(` Found ${circuits.length} circuit(s)`) if (this.options.dryRun) { - console.log('\n Would build:', circuits.map((c) => `${c.environment}/${c.name}`).join(', ')) + console.log('\n Would build:', circuits.map((c) => `${c.group}/${c.name}`).join(', ')) return result } @@ -127,15 +126,15 @@ class NoirCircuitBuilder { const circuits: CircuitInfo[] = [] if (!existsSync(this.circuitsDir)) return circuits - for (const env of this.options.environments ?? ALL_ENVIRONMENTS) { - const envDir = join(this.circuitsDir, env) - if (!existsSync(envDir)) continue + for (const group of this.options.groups ?? ALL_GROUPS) { + const groupDir = join(this.circuitsDir, group) + if (!existsSync(groupDir)) continue - for (const entry of readdirSync(envDir)) { - const circuitPath = join(envDir, entry) + for (const entry of readdirSync(groupDir)) { + const circuitPath = join(groupDir, entry) if (statSync(circuitPath).isDirectory() && existsSync(join(circuitPath, 'Nargo.toml'))) { if (!this.options.circuits || this.options.circuits.includes(entry)) { - circuits.push({ name: entry, environment: env, path: circuitPath }) + circuits.push({ name: entry, group, path: circuitPath }) } } } @@ -147,15 +146,15 @@ class NoirCircuitBuilder { const packageName = this.getPackageName(circuit.path) const result: CompiledCircuit = { name: circuit.name, - environment: circuit.environment, + group: circuit.group, artifacts: {}, checksums: {}, } execSync('nargo compile', { cwd: circuit.path, stdio: 'pipe' }) - const envDir = join(this.circuitsDir, circuit.environment) - const targetDirs = [join(envDir, 'target'), join(this.circuitsDir, 'target'), join(circuit.path, 'target')] + const groupDir = join(this.circuitsDir, circuit.group) + const targetDirs = [join(groupDir, 'target'), join(this.circuitsDir, 'target'), join(circuit.path, 'target')] let jsonFile: string | null = null let targetDir: string | null = null @@ -170,22 +169,24 @@ class NoirCircuitBuilder { } } - if (jsonFile && targetDir) { - this.sanitizePaths(jsonFile) - result.artifacts.json = jsonFile - result.checksums.json = this.checksum(jsonFile) + if (!jsonFile || !targetDir) { + throw new Error( + `${circuit.group}/${circuit.name}: compiled artifact not found. ` + `Searched for ${packageName}.json in: ${targetDirs.join(', ')}`, + ) + } - if (!this.options.skipVk) { - const vkFile = this.generateVk(jsonFile, targetDir, packageName) - if (vkFile) { - result.artifacts.vk = vkFile - result.checksums.vk = this.checksum(vkFile) - } + this.sanitizePaths(jsonFile) + result.artifacts.json = jsonFile + result.checksums.json = this.checksum(jsonFile) + + if (!this.options.skipVk) { + const vkFile = this.generateVk(jsonFile, targetDir, packageName) + if (vkFile) { + result.artifacts.vk = vkFile + result.checksums.vk = this.checksum(vkFile) } - console.log(` āœ“ ${circuit.environment}/${circuit.name}`) - } else { - console.log(` āš ļø ${circuit.environment}/${circuit.name}: no artifact found`) } + console.log(` āœ“ ${circuit.group}/${circuit.name}`) return result } @@ -201,7 +202,8 @@ class NoirCircuitBuilder { rmSync(defaultVk) } return existsSync(vkFile) ? vkFile : null - } catch { + } catch (err) { + console.error(`Error generating VK for ${jsonFile}:`, err) return null } } @@ -238,7 +240,7 @@ class NoirCircuitBuilder { const checksums: Record = {} for (const c of compiled) { - const prefix = `${c.environment}/${c.name}` + const prefix = `${c.group}/${c.name}` if (c.checksums.json && c.artifacts.json) { const f = `${prefix}/${basename(c.artifacts.json)}` checksums[f] = c.checksums.json @@ -264,7 +266,7 @@ class NoirCircuitBuilder { const outputDir = this.options.outputDir! for (const c of compiled) { if (!c.artifacts.json && !c.artifacts.vk) continue - const dir = join(outputDir, c.environment, c.name) + const dir = join(outputDir, c.group, c.name) mkdirSync(dir, { recursive: true }) if (c.artifacts.json) copyFileSync(c.artifacts.json, join(dir, basename(c.artifacts.json))) if (c.artifacts.vk) copyFileSync(c.artifacts.vk, join(dir, basename(c.artifacts.vk))) @@ -274,19 +276,22 @@ class NoirCircuitBuilder { computeSourceHash(): string { const hash = createHash('sha256') - const circuits = this.discoverCircuits().sort((a, b) => `${a.environment}/${a.name}`.localeCompare(`${b.environment}/${b.name}`)) + const circuits = this.discoverCircuits().sort((a, b) => `${a.group}/${a.name}`.localeCompare(`${b.group}/${b.name}`)) for (const c of circuits) this.hashDir(c.path, hash) return hash.digest('hex').substring(0, 16) } - private hashDir(dirPath: string, hash: ReturnType): void { + private hashDir(dirPath: string, hash: ReturnType, relativePath = ''): void { for (const entry of readdirSync(dirPath).sort()) { if (entry === 'target' || entry.startsWith('.')) continue const fullPath = join(dirPath, entry) + const entryRelativePath = relativePath ? `${relativePath}/${entry}` : entry const stat = statSync(fullPath) - if (stat.isDirectory()) this.hashDir(fullPath, hash) - else if (stat.isFile()) { - hash.update(entry) + if (stat.isDirectory()) { + hash.update(entryRelativePath + '/') + this.hashDir(fullPath, hash, entryRelativePath) + } else if (stat.isFile()) { + hash.update(entryRelativePath) hash.update(readFileSync(fullPath)) } } @@ -304,24 +309,6 @@ class NoirCircuitBuilder { if (output) appendFileSync(output, lines.join('\n') + '\n') else console.log('\nšŸ“‹ CI Output:', lines.join(', ')) } - - generateManifest(version: string): string { - const outputDir = this.options.outputDir! - mkdirSync(outputDir, { recursive: true }) - const manifest = { - version, - generatedAt: new Date().toISOString(), - sourceHash: this.computeSourceHash(), - circuits: this.discoverCircuits().map((c) => ({ - name: c.name, - environment: c.environment, - artifacts: { json: `${c.environment}/${c.name}/${c.name}.json`, vk: `${c.environment}/${c.name}/${c.name}.vk` }, - })), - } - const path = join(outputDir, 'manifest.json') - writeFileSync(path, JSON.stringify(manifest, null, 2) + '\n') - return path - } } // CLI @@ -329,7 +316,6 @@ async function main() { const args = process.argv.slice(2) const options: BuildOptions = {} let command = 'build' - let version: string | undefined for (let i = 0; i < args.length; i++) { const arg = args[i] @@ -337,16 +323,13 @@ async function main() { showHelp() process.exit(0) } else if (arg === '--dry-run') options.dryRun = true - else if (arg === '-v' || arg === '--verbose') options.verbose = true else if (arg === '--skip-checksums') options.skipChecksums = true else if (arg === '--skip-vk') options.skipVk = true - else if (arg === '--skip-production') options.environments = [ENVIRONMENTS.INSECURE] else if (arg === '--no-clean') options.clean = false - else if (arg === '--env') options.environments = args[++i]?.split(',') as Environment[] + else if (arg === '--group') options.groups = args[++i]?.split(',') as CircuitGroup[] else if (arg === '--circuit') (options.circuits ??= []).push(args[++i]) else if (arg === '-o' || arg === '--output') options.outputDir = resolve(args[++i]) - else if (arg === '--version') version = args[++i] - else if (['hash', 'manifest', 'build'].includes(arg)) command = arg + else if (['hash', 'build'].includes(arg)) command = arg } const builder = new NoirCircuitBuilder(undefined, options) @@ -355,12 +338,6 @@ async function main() { const hash = builder.computeSourceHash() console.log(hash) if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, `source_hash=${hash}\n`) - } else if (command === 'manifest') { - if (!version) { - console.error('āŒ --version required') - process.exit(1) - } - console.log(`āœ… Manifest: ${builder.generateManifest(version)}`) } else { const result = await builder.buildAll() builder.writeGitHubOutput(result) @@ -372,23 +349,20 @@ function showHelp() { console.log(` Usage: build-circuits [command] [options] -Commands: build (default), hash, manifest +Commands: build (default), hash Options: - --skip-production Only build insecure circuits + --group Circuit groups (comma-separated: dkg,threshold) + --circuit Build specific circuit(s) --skip-vk Skip verification key generation --skip-checksums Skip checksum generation - --env Environments (comma-separated: insecure,production) - --circuit Build specific circuit(s) -o, --output Output directory (default: dist/circuits) - --version Version for manifest --dry-run Show what would be built --no-clean Don't clean output directory - -v, --verbose Verbose output -h, --help Show help `) } if (require.main === module) main() -export { NoirCircuitBuilder, BuildOptions, BuildResult, CompiledCircuit, CircuitInfo, Environment, ENVIRONMENTS } +export { NoirCircuitBuilder, BuildOptions, BuildResult, CompiledCircuit, CircuitInfo, CircuitGroup, CIRCUIT_GROUPS } diff --git a/scripts/circuit-artifacts.ts b/scripts/circuit-artifacts.ts index 19e2ec3ae1..8ad6933cf9 100644 --- a/scripts/circuit-artifacts.ts +++ b/scripts/circuit-artifacts.ts @@ -18,7 +18,7 @@ const runV = (cmd: string, cwd = ROOT) => execSync(cmd, { cwd, stdio: 'inherit' async function push() { if (!existsSync(DIST)) { - console.error('āŒ No artifacts. Run: pnpm build:circuits --skip-production') + console.error('āŒ No artifacts. Run: pnpm build:circuits') process.exit(1) } @@ -40,7 +40,9 @@ async function push() { run(`git checkout -b ${BRANCH}`, tmp) } - cpSync(DIST, tmp, { recursive: true }) + for (const entry of readdirSync(DIST)) { + cpSync(join(DIST, entry), join(tmp, entry), { recursive: true }) + } writeFileSync(join(tmp, 'SOURCE_HASH'), hash) run('git add -A', tmp) From 9442ea439fc19e3987d411b8844052860a0611e7 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:49:24 +0000 Subject: [PATCH 4/5] chore: add aggregation circuit --- scripts/build-circuits.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build-circuits.ts b/scripts/build-circuits.ts index b42fef12b3..21c460e34c 100644 --- a/scripts/build-circuits.ts +++ b/scripts/build-circuits.ts @@ -11,9 +11,9 @@ import { appendFileSync, copyFileSync, existsSync, mkdirSync, readdirSync, readF import { basename, join, resolve } from 'path' // Types & Constants -const CIRCUIT_GROUPS = { DKG: 'dkg', THRESHOLD: 'threshold' } as const +const CIRCUIT_GROUPS = { DKG: 'dkg', THRESHOLD: 'threshold', AGGREGATION: 'recursive_aggregation' } as const type CircuitGroup = (typeof CIRCUIT_GROUPS)[keyof typeof CIRCUIT_GROUPS] -const ALL_GROUPS: CircuitGroup[] = [CIRCUIT_GROUPS.DKG, CIRCUIT_GROUPS.THRESHOLD] +const ALL_GROUPS: CircuitGroup[] = [CIRCUIT_GROUPS.DKG, CIRCUIT_GROUPS.THRESHOLD, CIRCUIT_GROUPS.AGGREGATION] interface CircuitInfo { name: string From d73e6bf10583dae2aa11e118b24dc7ec467e4d3a Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:55:18 +0000 Subject: [PATCH 5/5] chore: gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e25779c550..497c10b426 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ target *.DS_Store -dist \ No newline at end of file +dist