Skip to content

Commit

Permalink
chore: Initial code to generate ts code from Noir ABI (#2750)
Browse files Browse the repository at this point in the history
This is some rough code to generate typescript bindings for a given
abi.json


# Checklist:
Remove the checklist to signal you've completed it. Enable auto-merge if
the PR is ready to merge.
- [ ] If the pull request requires a cryptography review (e.g.
cryptographic algorithm implementations) I have added the 'crypto' tag.
- [ ] I have reviewed my diff in github, line by line and removed
unexpected formatting changes, testing logs, or commented-out code.
- [ ] Every change is related to the PR description.
- [ ] I have
[linked](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue)
this pull request to relevant issues (if any exist).

---------

Co-authored-by: sirasistant <sirasistant@gmail.com>
  • Loading branch information
kevaundray and sirasistant authored Oct 10, 2023
1 parent 64590fa commit 71d543a
Show file tree
Hide file tree
Showing 15 changed files with 960 additions and 11 deletions.
2 changes: 2 additions & 0 deletions yarn-project/noir-compiler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export { generateNoirContractInterface } from './contract-interface-gen/noir.js'
export { generateTypescriptContractInterface } from './contract-interface-gen/typescript.js';
export { generateAztecAbi };

export * from './noir_artifact.js';

/**
* Compile Aztec.nr contracts in project path using a nargo binary available in the shell.
* @param projectPath - Path to project.
Expand Down
18 changes: 17 additions & 1 deletion yarn-project/noir-compiler/src/noir_artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ABIParameter, ABIType, DebugFileMap, DebugInfo } from '@aztec/foundatio
type NoirFunctionType = 'Open' | 'Secret' | 'Unconstrained';

/** The ABI of an Aztec.nr function. */
interface NoirFunctionAbi {
export interface NoirFunctionAbi {
/** The parameters of the function. */
parameters: ABIParameter[];
/** The witness indices of the parameters. Indexed by parameter name. */
Expand Down Expand Up @@ -47,6 +47,22 @@ export interface NoirCompiledContract {
functions: NoirFunctionEntry[];
}

/**
* The compilation result of an Aztec.nr contract.
*/
export interface NoirCompiledCircuit {
/** The hash of the circuit. */
hash: number;
/** Compilation backend. */
backend: string;
/**
* The ABI of the function.
*/
abi: NoirFunctionAbi;
/** The bytecode of the circuit in base64. */
bytecode: string;
}

/**
* The debug metadata of an Aztec.nr contract.
*/
Expand Down
5 changes: 3 additions & 2 deletions yarn-project/noir-private-kernel/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
dist
.yarn
proofs/
Prover.toml
Verifier.toml
3 changes: 2 additions & 1 deletion yarn-project/noir-private-kernel/.prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
src/crates
src/target
src/target
src/types
4 changes: 4 additions & 0 deletions yarn-project/noir-private-kernel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"clean": "rm -rf ./dest .tsbuildinfo",
"formatting": "run -T prettier --check ./src && run -T eslint ./src",
"formatting:fix": "run -T prettier -w ./src",
"noir:build": "cd src && nargo compile",
"noir:types": "yarn ts-node --esm src/scripts/generate_ts_from_abi.ts",
"test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --passWithNoTests"
},
"inherits": [
Expand All @@ -26,7 +28,9 @@
"rootDir": "./src"
},
"dependencies": {
"@aztec/circuits.js": "workspace:^",
"@aztec/foundation": "workspace:^",
"@aztec/noir-compiler": "workspace:^",
"@noir-lang/acvm_js": "^0.28.0",
"@noir-lang/backend_barretenberg": "^0.7.10",
"@noir-lang/noir_js": "^0.16.0",
Expand Down
9 changes: 5 additions & 4 deletions yarn-project/noir-private-kernel/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { NoirCompiledCircuit } from '@aztec/noir-compiler';

import PrivateKernelInitJson from './target/private_kernel_init.json' assert { type: 'json' };
import PrivateKernelInnerJson from './target/private_kernel_inner.json' assert { type: 'json' };
import PrivateKernelOrderingJson from './target/private_kernel_ordering.json' assert { type: 'json' };

// TODO add types for noir circuit artifacts
export const PrivateKernelInitArtifact = PrivateKernelInitJson;
export const PrivateKernelInitArtifact = PrivateKernelInitJson as NoirCompiledCircuit;

export const PrivateKernelInnerArtifact = PrivateKernelInnerJson;
export const PrivateKernelInnerArtifact = PrivateKernelInnerJson as NoirCompiledCircuit;

export const PrivateKernelOrderingArtifact = PrivateKernelOrderingJson;
export const PrivateKernelOrderingArtifact = PrivateKernelOrderingJson as NoirCompiledCircuit;
202 changes: 202 additions & 0 deletions yarn-project/noir-private-kernel/src/scripts/generate_ts_from_abi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { ABIType } from '@aztec/foundation/abi';
import { createConsoleLogger } from '@aztec/foundation/log';
import { NoirCompiledCircuit, NoirFunctionAbi } from '@aztec/noir-compiler';

import fs from 'fs/promises';

const log = createConsoleLogger('aztec:noir-contracts');

/**
* Keep track off all of the Noir primitive types that were used.
* Most of these will not have a 1-1 definition in TypeScript,
* so we will need to generate type aliases for them.
*
* We want to generate type aliases
* for specific types that are used in the ABI.
*
* For example:
* - If `Field` is used we want to alias that
* with `number`.
* - If `u32` is used we want to alias that with `number` too.
*/
type PrimitiveTypesUsed = {
/**
* The name of the type alias that we will generate.
*/
aliasName: string;
/**
* The TypeScript type that we will alias to.
*/
tsType: string;
};

const noirPrimitiveTypesToTsTypes = new Map<string, PrimitiveTypesUsed>();

/**
* Typescript does not allow us to check for equality of non-primitive types
* easily, so we create a addIfUnique function that will only add an item
* to the map if it is not already there by using JSON.stringify.
* @param item - The item to add to the map.
*/
function addIfUnique(item: PrimitiveTypesUsed) {
const key = JSON.stringify(item);
if (!noirPrimitiveTypesToTsTypes.has(key)) {
noirPrimitiveTypesToTsTypes.set(key, item);
}
}

/**
* Converts an ABI type to a TypeScript type.
* @param type - The ABI type to convert.
* @returns The typescript code to define the type.
*/
function abiTypeToTs(type: ABIType): string {
switch (type.kind) {
case 'integer': {
let tsIntType = '';
if (type.sign === 'signed') {
tsIntType = `i${type.width}`;
} else {
tsIntType = `u${type.width}`;
}
addIfUnique({ aliasName: tsIntType, tsType: 'number' });
return tsIntType;
}
case 'boolean':
return `boolean`;
case 'array':
return `${abiTypeToTs(type.type)}[]`;
case 'struct':
return getLastComponentOfPath(type.path);
case 'field':
addIfUnique({ aliasName: 'Field', tsType: 'number' });
return 'Field';
default:
throw new Error(`Unknown ABI type ${type}`);
}
}

/**
* Returns the last component of a path, e.g. "foo::bar::baz" -\> "baz"
* Note: that if we have a path such as "Baz", we will return "Baz".
*
* Since these paths corresponds to structs, we can assume that we
* cannot have "foo::bar::".
*
* We also make the assumption that since these paths are coming from
* Noir, then we will not have two paths that look like this:
* - foo::bar::Baz
* - cat::dog::Baz
* ie the last component of the path (struct name) is enough to uniquely identify
* the whole path.
*
* TODO: We should double check this assumption when we use type aliases,
* I expect that `foo::bar::Baz as Dog` would effectively give `foo::bar::Dog`
* @param str - The path to get the last component of.
* @returns The last component of the path.
*/
function getLastComponentOfPath(str: string): string {
const parts = str.split('::');
const lastPart = parts[parts.length - 1];
return lastPart;
}

/**
* Generates TypeScript interfaces for the structs used in the ABI.
* @param type - The ABI type to generate the interface for.
* @param output - The set of structs that we have already generated bindings for.
* @returns The TypeScript code to define the struct.
*/
function generateStructInterfaces(type: ABIType, output: Set<string>): string {
let result = '';

// Edge case to handle the array of structs case.
if (type.kind === 'array' && type.type.kind === 'struct' && !output.has(getLastComponentOfPath(type.type.path))) {
result += generateStructInterfaces(type.type, output);
}
if (type.kind !== 'struct') return result;

// List of structs encountered while viewing this type that we need to generate
// bindings for.
const typesEncountered = new Set<ABIType>();

// Codegen the struct and then its fields, so that the structs fields
// are defined before the struct itself.
let codeGeneratedStruct = '';
let codeGeneratedStructFields = '';

const structName = getLastComponentOfPath(type.path);
if (!output.has(structName)) {
codeGeneratedStruct += `interface ${structName} {\n`;
for (const field of type.fields) {
codeGeneratedStruct += ` ${field.name}: ${abiTypeToTs(field.type)};\n`;
typesEncountered.add(field.type);
}
codeGeneratedStruct += `}\n\n`;
output.add(structName);

// Generate code for the encountered structs in the field above
for (const type of typesEncountered) {
codeGeneratedStructFields += generateStructInterfaces(type, output);
}
}

return codeGeneratedStructFields + '\n' + codeGeneratedStruct;
}

/**
* Generates a TypeScript interface for the ABI.
* @param abiObj - The ABI to generate the interface for.
* @returns The TypeScript code to define the interface.
*/
function generateTsInterface(abiObj: NoirFunctionAbi): string {
let result = ``;
const outputStructs = new Set<string>();

// Define structs for composite types
for (const param of abiObj.parameters) {
result += generateStructInterfaces(param.type, outputStructs);
}

// Generating Return type, if it exists
//
if (abiObj.return_type != null) {
result += generateStructInterfaces(abiObj.return_type, outputStructs);
result += `export interface ReturnType {\n`;
result += ` value: ${abiTypeToTs(abiObj.return_type)};\n`;
result += `}\n\n`;
}

// Generating Input type
result += 'export interface InputType {\n';
for (const param of abiObj.parameters) {
result += ` ${param.name}: ${abiTypeToTs(param.type)};\n`;
}
result += '}';

// Add the primitive Noir types that do not have a 1-1 mapping to TypeScript.
let primitiveTypeAliases = '';
for (const [, value] of noirPrimitiveTypesToTsTypes) {
primitiveTypeAliases += `\ntype ${value.aliasName} = ${value.tsType};`;
}

return `/* Autogenerated file, do not edit! */\n\n/* eslint-disable */\n` + primitiveTypeAliases + '\n' + result;
}

const circuits = ['private_kernel_init', 'private_kernel_inner', 'private_kernel_ordering'];

const main = async () => {
for (const circuit of circuits) {
const rawData = await fs.readFile(`./src/target/${circuit}.json`, 'utf-8');
const abiObj: NoirCompiledCircuit = JSON.parse(rawData);
const generatedInterface = generateTsInterface(abiObj.abi);
await fs.writeFile(`./src/types/${circuit}_types.ts`, generatedInterface);
}
};

try {
await main();
} catch (err: unknown) {
log(`Error generating types ${err}`);
process.exit(1);
}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Loading

0 comments on commit 71d543a

Please sign in to comment.