diff --git a/yarn-project/aztec/bootstrap.sh b/yarn-project/aztec/bootstrap.sh new file mode 100755 index 000000000000..337b7e40d9c3 --- /dev/null +++ b/yarn-project/aztec/bootstrap.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +source $(git rev-parse --show-toplevel)/ci3/source_bootstrap + +repo_root=$(git rev-parse --show-toplevel) +export NARGO=${NARGO:-$repo_root/noir/noir-repo/target/release/nargo} +export BB=${BB:-$repo_root/barretenberg/cpp/build/bin/bb} + +hash=$(../bootstrap.sh hash) + +function test_cmds { + echo "$hash:ISOLATE=1:NAME=aztec/src/cli/cmds/compile.test.ts NARGO=$NARGO BB=$BB yarn-project/scripts/run_test.sh aztec/src/cli/cmds/compile.test.ts" +} + +case "$cmd" in + "") + ;; + *) + default_cmd_handler "$@" + ;; +esac diff --git a/yarn-project/aztec/scripts/aztec.sh b/yarn-project/aztec/scripts/aztec.sh index 93016cf41334..a7bc4622ce1d 100755 --- a/yarn-project/aztec/scripts/aztec.sh +++ b/yarn-project/aztec/scripts/aztec.sh @@ -54,7 +54,7 @@ case $cmd in aztec start "$@" ;; - compile|new|init|flamegraph) + new|init|flamegraph) $script_dir/${cmd}.sh "$@" ;; *) diff --git a/yarn-project/aztec/scripts/compile.sh b/yarn-project/aztec/scripts/compile.sh deleted file mode 100755 index 7bec1e29d17f..000000000000 --- a/yarn-project/aztec/scripts/compile.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -NARGO=${NARGO:-nargo} -BB=${BB:-bb} - -# If help is requested, show Aztec-specific info then run nargo compile help and then exit in order to not trigger -# transpilation -for arg in "$@"; do - if [ "$arg" == "--help" ] || [ "$arg" == "-h" ]; then - cat << 'EOF' -Aztec Compile - Compile Aztec Noir contracts - -This command compiles Aztec Noir contracts using nargo and then automatically -postprocesses them to generate Aztec specific artifacts including: -- Transpiled contract artifacts -- Verification keys - -The compiled contracts will be placed in the target/ directory by default. - ---- -Underlying nargo compile options: - -EOF - nargo compile --help - exit 0 - fi -done - -# Run nargo compile. -$NARGO compile "$@" - -echo "Postprocessing contract..." -$BB aztec_process - -# Strip internal prefixes from all compiled contract JSONs in target directory -# TODO: This should be part of bb aztec_process! -for json in target/*.json; do - temp_file="${json}.tmp" - jq '.functions |= map(.name |= sub("^__aztec_nr_internals__"; ""))' "$json" > "$temp_file" - mv "$temp_file" "$json" -done - -echo "Compilation complete!" diff --git a/yarn-project/aztec/src/bin/index.ts b/yarn-project/aztec/src/bin/index.ts index d06b298d9add..90a915778b17 100644 --- a/yarn-project/aztec/src/bin/index.ts +++ b/yarn-project/aztec/src/bin/index.ts @@ -14,6 +14,7 @@ import { createConsoleLogger, createLogger } from '@aztec/foundation/log'; import { Command } from 'commander'; +import { injectCompileCommand } from '../cli/cmds/compile.js'; import { injectMigrateCommand } from '../cli/cmds/migrate_ha_db.js'; import { injectAztecCommands } from '../cli/index.js'; import { getCliVersion } from '../cli/release_version.js'; @@ -47,7 +48,7 @@ async function main() { const cliVersion = getCliVersion(); let program = new Command('aztec'); - program.description('Aztec command line interface').version(cliVersion); + program.description('Aztec command line interface').version(cliVersion).enablePositionalOptions(); program = injectAztecCommands(program, userLog, debugLogger); program = injectBuilderCommands(program); program = injectContractCommands(program, userLog, debugLogger); @@ -56,6 +57,7 @@ async function main() { program = injectAztecNodeCommands(program, userLog, debugLogger); program = injectMiscCommands(program, userLog); program = injectValidatorKeysCommands(program, userLog); + program = injectCompileCommand(program, userLog); program = injectMigrateCommand(program, userLog); await program.parseAsync(process.argv); diff --git a/yarn-project/aztec/src/cli/cli.ts b/yarn-project/aztec/src/cli/cli.ts index f086b852da31..1c79cf24f294 100644 --- a/yarn-project/aztec/src/cli/cli.ts +++ b/yarn-project/aztec/src/cli/cli.ts @@ -39,7 +39,6 @@ Additional commands: init [folder] [options] creates a new Aztec Noir project. new [options] creates a new Aztec Noir project in a new directory. - compile [options] compiles Aztec Noir contracts. test [options] starts a TXE and runs "nargo test" using it as the oracle resolver. `, ); diff --git a/yarn-project/aztec/src/cli/cmds/compile.test.ts b/yarn-project/aztec/src/cli/cmds/compile.test.ts new file mode 100644 index 000000000000..8a0af802b005 --- /dev/null +++ b/yarn-project/aztec/src/cli/cmds/compile.test.ts @@ -0,0 +1,82 @@ +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { execFileSync } from 'child_process'; +import { existsSync, readFileSync, rmSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '../../..'); +const CLI = join(PACKAGE_ROOT, 'dest/bin/index.js'); +const WORKSPACE = join(PACKAGE_ROOT, 'test/mixed-workspace'); +const TARGET = join(WORKSPACE, 'target'); +const CODEGEN_OUT = join(WORKSPACE, 'codegen-output'); + +// Compiles a mixed workspace containing both a contract and a plain circuit, +// then runs codegen. Validates that: +// - Contract artifacts have a functions array and are transpiled +// - Program (circuit) artifacts do not have functions and are not transpiled +// - Codegen produces TypeScript only for contracts, not circuits +describe('aztec compile integration', () => { + beforeAll(() => { + cleanupArtifacts(); + runCompile(); + runCodegen(); + }, 120_000); + + afterAll(() => { + cleanupArtifacts(); + }); + + it('contract artifact has functions array', () => { + const artifact = JSON.parse(readFileSync(join(TARGET, 'simple_contract-SimpleContract.json'), 'utf-8')); + expect(Array.isArray(artifact.functions)).toBe(true); + expect(artifact.functions.length).toBeGreaterThan(0); + }); + + it('program artifact does not have functions', () => { + const artifact = JSON.parse(readFileSync(join(TARGET, 'simple_circuit.json'), 'utf-8')); + expect(artifact.functions).toBeUndefined(); + }); + + it('contract artifact was transpiled', () => { + const artifact = JSON.parse(readFileSync(join(TARGET, 'simple_contract-SimpleContract.json'), 'utf-8')); + expect(artifact.transpiled).toBe(true); + }); + + it('program artifact was not transpiled', () => { + const artifact = JSON.parse(readFileSync(join(TARGET, 'simple_circuit.json'), 'utf-8')); + expect(artifact.transpiled).toBeFalsy(); + }); + + it('codegen produced TypeScript for contract', () => { + expect(existsSync(join(CODEGEN_OUT, 'SimpleContract.ts'))).toBe(true); + }); + + it('codegen did not produce TypeScript for circuit', () => { + expect(existsSync(join(CODEGEN_OUT, 'SimpleCircuit.ts'))).toBe(false); + }); +}); + +function cleanupArtifacts() { + rmSync(TARGET, { recursive: true, force: true }); + rmSync(CODEGEN_OUT, { recursive: true, force: true }); + rmSync(join(WORKSPACE, 'codegenCache.json'), { force: true }); +} + +function runCompile() { + try { + execFileSync('node', [CLI, 'compile'], { cwd: WORKSPACE, stdio: 'pipe' }); + } catch (e: any) { + throw new Error(`compile failed:\n${e.stderr?.toString() ?? e.message}`); + } +} + +function runCodegen() { + try { + execFileSync('node', [CLI, 'codegen', 'target', '-o', 'codegen-output', '-f'], { + cwd: WORKSPACE, + stdio: 'pipe', + }); + } catch (e: any) { + throw new Error(`codegen failed:\n${e.stderr?.toString() ?? e.message}`); + } +} diff --git a/yarn-project/aztec/src/cli/cmds/compile.ts b/yarn-project/aztec/src/cli/cmds/compile.ts new file mode 100644 index 000000000000..9fb8aba37f03 --- /dev/null +++ b/yarn-project/aztec/src/cli/cmds/compile.ts @@ -0,0 +1,107 @@ +import type { LogFn } from '@aztec/foundation/log'; + +import { execFileSync, spawn } from 'child_process'; +import type { Command } from 'commander'; +import { readFile, readdir, writeFile } from 'fs/promises'; +import { join } from 'path'; + +/** Spawns a command with inherited stdio and rejects on non-zero exit. */ +function run(cmd: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn(cmd, args, { stdio: 'inherit' }); + child.on('error', reject); + child.on('close', code => { + if (code !== 0) { + reject(new Error(`${cmd} exited with code ${code}`)); + } else { + resolve(); + } + }); + }); +} + +/** Returns paths to contract artifacts in the target directory. + * Contract artifacts are identified by having a `functions` array in the JSON. + */ +async function collectContractArtifacts(): Promise { + let files: string[]; + try { + files = await readdir('target'); + } catch (err: any) { + if (err?.code === 'ENOENT') { + return []; + } + throw new Error(`Failed to read target directory: ${err.message}`); + } + + const artifacts: string[] = []; + for (const file of files) { + if (!file.endsWith('.json')) { + continue; + } + const filePath = join('target', file); + const content = JSON.parse(await readFile(filePath, 'utf-8')); + if (Array.isArray(content.functions)) { + artifacts.push(filePath); + } + } + return artifacts; +} + +/** Strips the `__aztec_nr_internals__` prefix from function names in contract artifacts. */ +async function stripInternalPrefixes(artifactPaths: string[]): Promise { + for (const path of artifactPaths) { + const artifact = JSON.parse(await readFile(path, 'utf-8')); + for (const fn of artifact.functions) { + if (typeof fn.name === 'string') { + fn.name = fn.name.replace(/^__aztec_nr_internals__/, ''); + } + } + await writeFile(path, JSON.stringify(artifact, null, 2) + '\n'); + } +} + +/** Compiles Aztec Noir contracts and postprocesses artifacts. */ +async function compileAztecContract(nargoArgs: string[], log: LogFn): Promise { + const nargo = process.env.NARGO ?? 'nargo'; + const bb = process.env.BB ?? 'bb'; + + await run(nargo, ['compile', ...nargoArgs]); + + const artifacts = await collectContractArtifacts(); + + if (artifacts.length > 0) { + log('Postprocessing contracts...'); + const bbArgs = artifacts.flatMap(a => ['-i', a]); + await run(bb, ['aztec_process', ...bbArgs]); + + // TODO: This should be part of bb aztec_process! + await stripInternalPrefixes(artifacts); + } + + log('Compilation complete!'); +} + +export function injectCompileCommand(program: Command, log: LogFn): Command { + program + .command('compile') + .argument('[nargo-args...]') + .passThroughOptions() + .allowUnknownOption() + .description( + 'Compile Aztec Noir contracts using nargo and postprocess them to generate transpiled artifacts and verification keys. All options are forwarded to nargo compile.', + ) + .addHelpText('after', () => { + // Show nargo's own compile options so users see all available flags in one place. + const nargo = process.env.NARGO ?? 'nargo'; + try { + const output = execFileSync(nargo, ['compile', '--help'], { encoding: 'utf-8' }); + return `\nUnderlying nargo compile options:\n\n${output}`; + } catch { + return '\n(Run "nargo compile --help" to see available nargo options)'; + } + }) + .action((nargoArgs: string[]) => compileAztecContract(nargoArgs, log)); + + return program; +} diff --git a/yarn-project/aztec/test/mixed-workspace/.gitignore b/yarn-project/aztec/test/mixed-workspace/.gitignore new file mode 100644 index 000000000000..8515795b7b4d --- /dev/null +++ b/yarn-project/aztec/test/mixed-workspace/.gitignore @@ -0,0 +1,3 @@ +target/ +codegen-output/ +codegenCache.json diff --git a/yarn-project/aztec/test/mixed-workspace/Nargo.toml b/yarn-project/aztec/test/mixed-workspace/Nargo.toml new file mode 100644 index 000000000000..2ef7da4e84a0 --- /dev/null +++ b/yarn-project/aztec/test/mixed-workspace/Nargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["simple_contract", "simple_circuit"] diff --git a/yarn-project/aztec/test/mixed-workspace/README.md b/yarn-project/aztec/test/mixed-workspace/README.md new file mode 100644 index 000000000000..83f385759655 --- /dev/null +++ b/yarn-project/aztec/test/mixed-workspace/README.md @@ -0,0 +1,26 @@ +# Mixed Workspace Test + +Regression test for `aztec compile` and `aztec codegen` in Nargo workspaces that +contain both Aztec contracts and plain Noir circuits. + +## Problem + +Both `aztec compile` and `aztec codegen` assumed every `.json` in `target/` is a +contract artifact. When a workspace also contains `type = "bin"` packages, the +resulting program artifacts lack `functions`/`name` fields, causing: + +- `bb aztec_process` to fail trying to transpile a program artifact +- The jq postprocessing step to fail on missing `.functions` +- `codegen` to crash calling `loadContractArtifact()` on a program artifact + +## What the test checks + +`yarn-project/aztec/src/cli/cmds/compile.test.ts` runs compile and codegen on +this workspace and verifies: + +1. Compilation succeeds without errors +2. Both artifacts exist in `target/` +3. The contract artifact was postprocessed (has `transpiled` field) +4. The program artifact was not modified (no `transpiled` field) +5. Codegen generates a TypeScript wrapper only for the contract +6. No TypeScript wrapper is generated for the program artifact diff --git a/yarn-project/aztec/test/mixed-workspace/simple_circuit/Nargo.toml b/yarn-project/aztec/test/mixed-workspace/simple_circuit/Nargo.toml new file mode 100644 index 000000000000..a74e4a284148 --- /dev/null +++ b/yarn-project/aztec/test/mixed-workspace/simple_circuit/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "simple_circuit" +authors = [""] +compiler_version = ">=0.25.0" +type = "bin" + +[dependencies] diff --git a/yarn-project/aztec/test/mixed-workspace/simple_circuit/src/main.nr b/yarn-project/aztec/test/mixed-workspace/simple_circuit/src/main.nr new file mode 100644 index 000000000000..e149eb109fca --- /dev/null +++ b/yarn-project/aztec/test/mixed-workspace/simple_circuit/src/main.nr @@ -0,0 +1,3 @@ +fn main(x: Field) { + assert(x != 0); +} diff --git a/yarn-project/aztec/test/mixed-workspace/simple_contract/Nargo.toml b/yarn-project/aztec/test/mixed-workspace/simple_contract/Nargo.toml new file mode 100644 index 000000000000..681c51353ea5 --- /dev/null +++ b/yarn-project/aztec/test/mixed-workspace/simple_contract/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "simple_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../../../noir-projects/aztec-nr/aztec" } diff --git a/yarn-project/aztec/test/mixed-workspace/simple_contract/src/main.nr b/yarn-project/aztec/test/mixed-workspace/simple_contract/src/main.nr new file mode 100644 index 000000000000..43c4331608db --- /dev/null +++ b/yarn-project/aztec/test/mixed-workspace/simple_contract/src/main.nr @@ -0,0 +1,11 @@ +use aztec::macros::aztec; + +#[aztec] +pub contract SimpleContract { + use aztec::macros::functions::external; + + #[external("private")] + fn private_function() -> Field { + 0 + } +} diff --git a/yarn-project/bootstrap.sh b/yarn-project/bootstrap.sh index 71fb1e02237b..f96ea4c0ad1c 100755 --- a/yarn-project/bootstrap.sh +++ b/yarn-project/bootstrap.sh @@ -221,6 +221,9 @@ function test_cmds { # Uses mocha for browser tests, so we have to treat it differently. echo "$hash:ISOLATE=1 cd yarn-project/kv-store && yarn test" + # Aztec CLI tests + aztec/bootstrap.sh test_cmds + if [[ "${TARGET_BRANCH:-}" =~ ^v[0-9]+$ ]]; then echo "$hash yarn-project/scripts/run_test.sh aztec/src/testnet_compatibility.test.ts" echo "$hash yarn-project/scripts/run_test.sh aztec/src/mainnet_compatibility.test.ts" diff --git a/yarn-project/builder/src/contract-interface-gen/codegen.ts b/yarn-project/builder/src/contract-interface-gen/codegen.ts index 5321c5070d22..1137b81a6a6c 100644 --- a/yarn-project/builder/src/contract-interface-gen/codegen.ts +++ b/yarn-project/builder/src/contract-interface-gen/codegen.ts @@ -50,6 +50,12 @@ async function generateFromNoirAbi(outputPath: string, noirAbiPath: string, opts const file = await readFile(noirAbiPath, 'utf8'); const contract = JSON.parse(file); + + if (!Array.isArray(contract.functions)) { + console.log(`${fileName} is not a contract artifact. Skipping.`); + return; + } + const aztecAbi = loadContractArtifact(contract); await mkdir(outputPath, { recursive: true });