diff --git a/yarn-project/acir-simulator/src/client/private_execution.test.ts b/yarn-project/acir-simulator/src/client/private_execution.test.ts index 43a69221d78..ffe6c12abc7 100644 --- a/yarn-project/acir-simulator/src/client/private_execution.test.ts +++ b/yarn-project/acir-simulator/src/client/private_execution.test.ts @@ -18,6 +18,7 @@ import { computeContractAddressFromPartial, computeSecretMessageHash, computeUniqueCommitment, + computeVarArgsHash, siloCommitment, } from '@aztec/circuits.js/abis'; import { pedersenPlookupCommitInputs } from '@aztec/circuits.js/barretenberg'; @@ -32,6 +33,7 @@ import { DebugLogger, createDebugLogger } from '@aztec/foundation/log'; import { AppendOnlyTree, Pedersen, StandardTree, newTree } from '@aztec/merkle-tree'; import { ChildContractAbi, + ImportTestContractAbi, NonNativeTokenContractAbi, ParentContractAbi, PendingCommitmentsContractAbi, @@ -641,7 +643,50 @@ describe('Private Execution test suite', () => { }); }); - describe('consuming Messages', () => { + describe('nested calls through autogenerated interface', () => { + let args: any[]; + let argsHash: Fr; + let testCodeGenAbi: FunctionAbi; + + beforeAll(async () => { + // These args should match the ones hardcoded in importer contract + const dummyNote = { amount: 1, secretHash: 2 }; + const deepStruct = { aField: 1, aBool: true, aNote: dummyNote, manyNotes: [dummyNote, dummyNote, dummyNote] }; + args = [1, true, 1, [1, 2], dummyNote, deepStruct]; + testCodeGenAbi = TestContractAbi.functions.find(f => f.name === 'testCodeGen')!; + const serialisedArgs = encodeArguments(testCodeGenAbi, args); + argsHash = await computeVarArgsHash(await CircuitsWasm.get(), serialisedArgs); + }); + + it('test function should be directly callable', async () => { + logger(`Calling testCodeGen function`); + const result = await runSimulator({ args, abi: testCodeGenAbi }); + + expect(result.callStackItem.publicInputs.returnValues[0]).toEqual(argsHash); + }); + + it('test function should be callable through autogenerated interface', async () => { + const importerAddress = AztecAddress.random(); + const testAddress = AztecAddress.random(); + const parentAbi = ImportTestContractAbi.functions.find(f => f.name === 'main')!; + const testCodeGenSelector = generateFunctionSelector(testCodeGenAbi.name, testCodeGenAbi.parameters); + + oracle.getFunctionABI.mockResolvedValue(testCodeGenAbi); + oracle.getPortalContractAddress.mockResolvedValue(EthAddress.ZERO); + + logger(`Calling importer main function`); + const args = [testAddress]; + const result = await runSimulator({ args, abi: parentAbi, origin: importerAddress }); + + expect(result.callStackItem.publicInputs.returnValues[0]).toEqual(argsHash); + expect(oracle.getFunctionABI.mock.calls[0]).toEqual([testAddress, testCodeGenSelector]); + expect(oracle.getPortalContractAddress.mock.calls[0]).toEqual([testAddress]); + expect(result.nestedExecutions).toHaveLength(1); + expect(result.nestedExecutions[0].callStackItem.publicInputs.returnValues[0]).toEqual(argsHash); + }); + }); + + describe('consuming messages', () => { const contractAddress = defaultContractAddress; const recipientPk = PrivateKey.fromString('0c9ed344548e8f9ba8aa3c9f8651eaa2853130f6c1e9c050ccf198f7ea18a7ec'); diff --git a/yarn-project/end-to-end/src/e2e_nested_contract.test.ts b/yarn-project/end-to-end/src/e2e_nested_contract.test.ts index fba6e540515..fb8cdedd015 100644 --- a/yarn-project/end-to-end/src/e2e_nested_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_nested_contract.test.ts @@ -5,7 +5,7 @@ import { ContractAbi } from '@aztec/foundation/abi'; import { DebugLogger } from '@aztec/foundation/log'; import { toBigInt } from '@aztec/foundation/serialize'; import { ChildContractAbi, ParentContractAbi } from '@aztec/noir-contracts/artifacts'; -import { ChildContract, ParentContract } from '@aztec/noir-contracts/types'; +import { ChildContract, ImportTestContract, ParentContract, TestContract } from '@aztec/noir-contracts/types'; import { AztecRPC, TxStatus } from '@aztec/types'; import { setup } from './fixtures/utils.js'; @@ -17,14 +17,8 @@ describe('e2e_nested_contract', () => { let accounts: AztecAddress[]; let logger: DebugLogger; - let parentContract: ParentContract; - let childContract: ChildContract; - beforeEach(async () => { ({ aztecNode, aztecRpcServer, accounts, wallet, logger } = await setup()); - - parentContract = (await deployContract(ParentContractAbi)) as ParentContract; - childContract = (await deployContract(ChildContractAbi)) as ChildContract; }, 100_000); afterEach(async () => { @@ -34,129 +28,166 @@ describe('e2e_nested_contract', () => { } }); - const deployContract = async (abi: ContractAbi) => { - logger(`Deploying L2 contract ${abi.name}...`); - const deployer = new ContractDeployer(abi, aztecRpcServer); - const tx = deployer.deploy().send(); - - await tx.isMined({ interval: 0.1 }); - - const receipt = await tx.getReceipt(); - const contract = await Contract.create(receipt.contractAddress!, abi, wallet); - logger(`L2 contract ${abi.name} deployed at ${contract.address}`); - return contract; - }; - - const addressToField = (address: AztecAddress): bigint => Fr.fromBuffer(address.toBuffer()).value; - - const getChildStoredValue = (child: { address: AztecAddress }) => - aztecRpcServer.getPublicStorageAt(child.address, new Fr(1)).then(x => toBigInt(x!)); - - /** - * Milestone 3. - */ - it('performs nested calls', async () => { - const tx = parentContract.methods - .entryPoint(childContract.address, Fr.fromBuffer(childContract.methods.value.selector)) - .send({ origin: accounts[0] }); - - await tx.isMined({ interval: 0.1 }); - const receipt = await tx.getReceipt(); - - expect(receipt.status).toBe(TxStatus.MINED); - }, 100_000); - - it('performs public nested calls', async () => { - const tx = parentContract.methods - .pubEntryPoint(childContract.address, Fr.fromBuffer(childContract.methods.pubValue.selector), 42n) - .send({ origin: accounts[0] }); - - await tx.isMined({ interval: 0.1 }); - const receipt = await tx.getReceipt(); - - expect(receipt.status).toBe(TxStatus.MINED); - }, 100_000); - - it('enqueues a single public call', async () => { - const tx = parentContract.methods - .enqueueCallToChild(childContract.address, Fr.fromBuffer(childContract.methods.pubStoreValue.selector), 42n) - .send({ origin: accounts[0] }); - - await tx.isMined({ interval: 0.1 }); - const receipt = await tx.getReceipt(); - expect(receipt.status).toBe(TxStatus.MINED); - - expect(await getChildStoredValue(childContract)).toEqual(42n); - }, 100_000); - - // Fails with "solver opcode resolution error: cannot solve opcode: expression has too many unknowns %EXPR [ 0 ]%" - // See https://github.com/noir-lang/noir/issues/1347 - it.skip('enqueues multiple public calls', async () => { - const tx = parentContract.methods - .enqueueCallToChildTwice( - addressToField(childContract.address), - Fr.fromBuffer(childContract.methods.pubStoreValue.selector).value, - 42n, - ) - .send({ origin: accounts[0] }); - - await tx.isMined({ interval: 0.1 }); - const receipt = await tx.getReceipt(); - expect(receipt.status).toBe(TxStatus.MINED); - - expect(await getChildStoredValue(childContract)).toEqual(85n); - }, 100_000); - - it('enqueues a public call with nested public calls', async () => { - const tx = parentContract.methods - .enqueueCallToPubEntryPoint( - childContract.address, - Fr.fromBuffer(childContract.methods.pubStoreValue.selector), - 42n, - ) - .send({ origin: accounts[0] }); - - await tx.isMined({ interval: 0.1 }); - const receipt = await tx.getReceipt(); - expect(receipt.status).toBe(TxStatus.MINED); - - expect(await getChildStoredValue(childContract)).toEqual(42n); - }, 100_000); - - // Fails with "solver opcode resolution error: cannot solve opcode: expression has too many unknowns %EXPR [ 0 ]%" - // See https://github.com/noir-lang/noir/issues/1347 - it.skip('enqueues multiple public calls with nested public calls', async () => { - const tx = parentContract.methods - .enqueueCallsToPubEntryPoint( - childContract.address, - Fr.fromBuffer(childContract.methods.pubStoreValue.selector), - 42n, - ) - .send({ origin: accounts[0] }); - - await tx.isMined({ interval: 0.1 }); - const receipt = await tx.getReceipt(); - expect(receipt.status).toBe(TxStatus.MINED); - - expect(await getChildStoredValue(childContract)).toEqual(84n); - }, 100_000); + describe('parent manually calls child', () => { + let parentContract: ParentContract; + let childContract: ChildContract; + + beforeEach(async () => { + parentContract = (await deployContract(ParentContractAbi)) as ParentContract; + childContract = (await deployContract(ChildContractAbi)) as ChildContract; + }, 100_000); + + const deployContract = async (abi: ContractAbi) => { + logger(`Deploying L2 contract ${abi.name}...`); + const deployer = new ContractDeployer(abi, aztecRpcServer); + const tx = deployer.deploy().send(); + + await tx.isMined({ interval: 0.1 }); + + const receipt = await tx.getReceipt(); + const contract = await Contract.create(receipt.contractAddress!, abi, wallet); + logger(`L2 contract ${abi.name} deployed at ${contract.address}`); + return contract; + }; + + const addressToField = (address: AztecAddress): bigint => Fr.fromBuffer(address.toBuffer()).value; + + const getChildStoredValue = (child: { address: AztecAddress }) => + aztecRpcServer.getPublicStorageAt(child.address, new Fr(1)).then(x => toBigInt(x!)); + + /** + * Milestone 3. + */ + it('performs nested calls', async () => { + const tx = parentContract.methods + .entryPoint(childContract.address, Fr.fromBuffer(childContract.methods.value.selector)) + .send({ origin: accounts[0] }); + + await tx.isMined({ interval: 0.1 }); + const receipt = await tx.getReceipt(); + + expect(receipt.status).toBe(TxStatus.MINED); + }, 100_000); + + it('performs public nested calls', async () => { + const tx = parentContract.methods + .pubEntryPoint(childContract.address, Fr.fromBuffer(childContract.methods.pubValue.selector), 42n) + .send({ origin: accounts[0] }); + + await tx.isMined({ interval: 0.1 }); + const receipt = await tx.getReceipt(); + + expect(receipt.status).toBe(TxStatus.MINED); + }, 100_000); + + it('enqueues a single public call', async () => { + const tx = parentContract.methods + .enqueueCallToChild(childContract.address, Fr.fromBuffer(childContract.methods.pubStoreValue.selector), 42n) + .send({ origin: accounts[0] }); + + await tx.isMined({ interval: 0.1 }); + const receipt = await tx.getReceipt(); + expect(receipt.status).toBe(TxStatus.MINED); + + expect(await getChildStoredValue(childContract)).toEqual(42n); + }, 100_000); + + // Fails with "solver opcode resolution error: cannot solve opcode: expression has too many unknowns %EXPR [ 0 ]%" + // See https://github.com/noir-lang/noir/issues/1347 + it.skip('enqueues multiple public calls', async () => { + const tx = parentContract.methods + .enqueueCallToChildTwice( + addressToField(childContract.address), + Fr.fromBuffer(childContract.methods.pubStoreValue.selector).value, + 42n, + ) + .send({ origin: accounts[0] }); + + await tx.isMined({ interval: 0.1 }); + const receipt = await tx.getReceipt(); + expect(receipt.status).toBe(TxStatus.MINED); + + expect(await getChildStoredValue(childContract)).toEqual(85n); + }, 100_000); + + it('enqueues a public call with nested public calls', async () => { + const tx = parentContract.methods + .enqueueCallToPubEntryPoint( + childContract.address, + Fr.fromBuffer(childContract.methods.pubStoreValue.selector), + 42n, + ) + .send({ origin: accounts[0] }); + + await tx.isMined({ interval: 0.1 }); + const receipt = await tx.getReceipt(); + expect(receipt.status).toBe(TxStatus.MINED); + + expect(await getChildStoredValue(childContract)).toEqual(42n); + }, 100_000); + + // Fails with "solver opcode resolution error: cannot solve opcode: expression has too many unknowns %EXPR [ 0 ]%" + // See https://github.com/noir-lang/noir/issues/1347 + it.skip('enqueues multiple public calls with nested public calls', async () => { + const tx = parentContract.methods + .enqueueCallsToPubEntryPoint( + childContract.address, + Fr.fromBuffer(childContract.methods.pubStoreValue.selector), + 42n, + ) + .send({ origin: accounts[0] }); + + await tx.isMined({ interval: 0.1 }); + const receipt = await tx.getReceipt(); + expect(receipt.status).toBe(TxStatus.MINED); + + expect(await getChildStoredValue(childContract)).toEqual(84n); + }, 100_000); + + // Regression for https://github.com/AztecProtocol/aztec-packages/issues/640 + // Fails with "solver opcode resolution error: cannot solve opcode: expression has too many unknowns %EXPR [ 0 ]%" + // See https://github.com/noir-lang/noir/issues/1347 + it.skip('reads fresh value after write within the same tx', async () => { + const tx = parentContract.methods + .pubEntryPointTwice( + addressToField(childContract.address), + Fr.fromBuffer(childContract.methods.pubStoreValue.selector).value, + 42n, + ) + .send({ origin: accounts[0] }); + + await tx.isMined({ interval: 0.1 }); + const receipt = await tx.getReceipt(); + + expect(receipt.status).toBe(TxStatus.MINED); + expect(await getChildStoredValue(childContract)).toEqual(85n); + }, 100_000); + }); - // Regression for https://github.com/AztecProtocol/aztec-packages/issues/640 - // Fails with "solver opcode resolution error: cannot solve opcode: expression has too many unknowns %EXPR [ 0 ]%" - // See https://github.com/noir-lang/noir/issues/1347 - it.skip('reads fresh value after write within the same tx', async () => { - const tx = parentContract.methods - .pubEntryPointTwice( - addressToField(childContract.address), - Fr.fromBuffer(childContract.methods.pubStoreValue.selector).value, - 42n, - ) - .send({ origin: accounts[0] }); - - await tx.isMined({ interval: 0.1 }); - const receipt = await tx.getReceipt(); - - expect(receipt.status).toBe(TxStatus.MINED); - expect(await getChildStoredValue(childContract)).toEqual(85n); - }, 100_000); + describe('importer uses autogenerated test contract interface', () => { + let importerContract: ImportTestContract; + let testContract: TestContract; + + beforeEach(async () => { + logger(`Deploying importer test contract`); + importerContract = await ImportTestContract.deploy(wallet).send().deployed(); + logger(`Deploying test contract`); + testContract = await TestContract.deploy(wallet).send().deployed(); + }, 30_000); + + it('calls a method with multiple arguments', async () => { + logger(`Calling main on importer contract`); + await importerContract.methods.main(testContract.address).send().wait(); + }, 30_000); + + it('calls a method no arguments', async () => { + logger(`Calling noargs on importer contract`); + await importerContract.methods.callNoArgs(testContract.address).send().wait(); + }, 30_000); + + it('calls an open function', async () => { + logger(`Calling openfn on importer contract`); + await importerContract.methods.callOpenFn(testContract.address).send().wait(); + }, 30_000); + }); }); diff --git a/yarn-project/foundation/src/abi/abi_coder.ts b/yarn-project/foundation/src/abi/abi_coder.ts index c33e3db4cd9..5fd4ccd48fa 100644 --- a/yarn-project/foundation/src/abi/abi_coder.ts +++ b/yarn-project/foundation/src/abi/abi_coder.ts @@ -34,7 +34,7 @@ export function computeFunctionSelector(signature: string, size: number) { */ export function generateFunctionSelector(name: string, parameters: ABIParameter[]) { const signature = computeFunctionSignature(name, parameters); - return keccak(Buffer.from(signature)).slice(0, 4); + return computeFunctionSelector(signature, 4); } /** diff --git a/yarn-project/noir-compiler/package.json b/yarn-project/noir-compiler/package.json index 93c3b9a6e86..24c5ace3f5f 100644 --- a/yarn-project/noir-compiler/package.json +++ b/yarn-project/noir-compiler/package.json @@ -40,7 +40,11 @@ "@noir-lang/noir_wasm": "0.5.1-9740f54", "commander": "^9.0.0", "fs-extra": "^11.1.1", + "lodash.camelcase": "^4.3.0", + "lodash.capitalize": "^4.2.1", "lodash.compact": "^3.0.1", + "lodash.times": "^4.3.2", + "lodash.upperfirst": "^4.3.1", "toml": "^3.0.0", "tslib": "^2.4.0" }, @@ -49,7 +53,11 @@ "@rushstack/eslint-patch": "^1.1.4", "@types/fs-extra": "^11.0.1", "@types/jest": "^29.5.0", + "@types/lodash.camelcase": "^4.3.7", + "@types/lodash.capitalize": "^4.2.7", "@types/lodash.compact": "^3.0.7", + "@types/lodash.times": "^4.3.7", + "@types/lodash.upperfirst": "^4.3.7", "@types/node": "^18.7.23", "jest": "^29.5.0", "ts-jest": "^29.1.0", diff --git a/yarn-project/noir-compiler/src/cli.ts b/yarn-project/noir-compiler/src/cli.ts index d35fff1cc0c..f335b8bdebb 100644 --- a/yarn-project/noir-compiler/src/cli.ts +++ b/yarn-project/noir-compiler/src/cli.ts @@ -7,7 +7,7 @@ import fs from 'fs/promises'; import nodePath from 'path'; import { ContractCompiler } from './compile.js'; -import { generateType } from './index.js'; +import { generateTSContractInterface } from './index.js'; const program = new Command(); const log = createConsoleLogger('noir-compiler-cli'); @@ -39,7 +39,7 @@ const main = async () => { .argument('[targetPath]', 'Path to the output file') .action(async (buildPath: string, targetPath: string) => { const artifact = readJSONSync(buildPath); - const output = generateType(artifact); + const output = generateTSContractInterface(artifact); await fs.writeFile(targetPath, output); log(`Written type for ${artifact.name} to ${targetPath}`); }); diff --git a/yarn-project/noir-compiler/src/contract-interface-gen/noir.ts b/yarn-project/noir-compiler/src/contract-interface-gen/noir.ts new file mode 100644 index 00000000000..2e38b9fea2c --- /dev/null +++ b/yarn-project/noir-compiler/src/contract-interface-gen/noir.ts @@ -0,0 +1,261 @@ +import { + ABIParameter, + ABIVariable, + ContractAbi, + FunctionAbi, + FunctionType, + StructType, + generateFunctionSelector, +} from '@aztec/foundation/abi'; + +import camelCase from 'lodash.camelcase'; +import compact from 'lodash.compact'; +import times from 'lodash.times'; +import upperFirst from 'lodash.upperfirst'; + +/** + * Returns whether this function type corresponds to a private call. + * @param functionType - The function type. + * @returns Whether this function type corresponds to a private call. + */ +function isPrivateCall(functionType: FunctionType) { + return functionType === FunctionType.SECRET; +} + +/** + * Generates a call to a private function using the context. + * @param selector - The selector of a function. + * @param functionType - Type of the function. + * @returns A code string. + */ +function generateCallStatement(selector: string, functionType: FunctionType) { + const callMethod = isPrivateCall(functionType) ? 'call_private_function' : 'call_public_function'; + return ` + context.${callMethod}(self.address, ${selector}, serialised_args)`; +} + +/** + * Formats a string as pascal case. + * @param str - A string. + * @returns A capitalised camelcase string. + */ +function toPascalCase(str: string) { + return upperFirst(camelCase(str)); +} + +/** + * Returns a struct name given a list of fragments. + * @param fragments - Fragments. + * @returns The concatenation of the capitalised fragments. + */ +function getStructName(...fragments: string[]) { + return fragments.map(toPascalCase).join('') + 'Struct'; +} + +/** + * Returns a Noir type name for the given ABI variable. + * @param param - ABI variable to translate to a Noir type name. + * @param parentNames - Function name or parent structs or arrays to use for struct qualified names. + * @returns A valid Noir basic type name or a name for a struct. + */ +function getTypeName(param: ABIVariable, ...parentNames: string[]): string { + const type = param.type; + switch (type.kind) { + case 'field': + return 'Field'; + case 'boolean': + return 'bool'; + case 'integer': + return `${type.sign === 'signed' ? 'i' : 'u'}${type.width}`; + case 'string': + throw new Error(`Strings not supported yet`); + case 'array': + return `[${getTypeName({ name: param.name, type: type.type }, ...parentNames)};${type.length}]`; + case 'struct': + return getStructName(param.name, ...parentNames); + default: + throw new Error(`Unknown type ${type}`); + } +} + +/** + * Generates a parameter string. + * @param param - ABI parameter. + * @param functionData - Parent function. + * @returns A Noir string with the param name and type to be used in a function call. + */ +function generateParameter(param: ABIParameter, functionData: FunctionAbi) { + const typename = getTypeName(param, functionData.name); + return `${param.name}: ${typename}`; +} + +/** + * Collects all parameters for a given function and flattens them according to how they should be serialised. + * @param parameters - Paramters for a function. + * @returns List of parameters flattened to basic data types. + */ +function collectParametersForSerialisation(parameters: ABIVariable[]) { + const flattened: string[] = []; + for (const parameter of parameters) { + const { name } = parameter; + if (parameter.type.kind === 'array') { + const nestedType = parameter.type.type; + const nested = times(parameter.type.length, i => + collectParametersForSerialisation([{ name: `${name}[${i}]`, type: nestedType }]), + ); + flattened.push(...nested.flat()); + } else if (parameter.type.kind === 'struct') { + const nested = parameter.type.fields.map(field => + collectParametersForSerialisation([{ name: `${name}.${field.name}`, type: field.type }]), + ); + flattened.push(...nested.flat()); + } else if (parameter.type.kind === 'string') { + throw new Error(`String not yet supported`); + } else if (parameter.type.kind === 'field') { + flattened.push(name); + } else { + flattened.push(`${name} as Field`); + } + } + return flattened; +} + +/** + * Generates Noir code for serialising the parameters into an array of fields. + * @param parameters - Parameters to serialise. + * @returns The serialisation code. + */ +function generateSerialisation(parameters: ABIParameter[]) { + const flattened = collectParametersForSerialisation(parameters); + const declaration = ` let mut serialised_args = [0; ${flattened.length}];`; + const lines = flattened.map((param, i) => ` serialised_args[${i}] = ${param};`); + return [declaration, ...lines].join('\n'); +} + +/** + * Generate a function interface for a particular function of the Noir Contract being processed. This function will be a method of the ContractInterface struct being created here. + * @param functionData - data relating to the function, which can be used to generate a callable Noir Function. + * @returns a code string. + */ +function generateFunctionInterface(functionData: FunctionAbi) { + const { name, parameters } = functionData; + const selector = '0x' + generateFunctionSelector(name, parameters).toString('hex'); + const serialisation = generateSerialisation(parameters); + const callStatement = generateCallStatement(selector, functionData.functionType); + const allParams = ['self', 'context: &mut Context', ...parameters.map(p => generateParameter(p, functionData))]; + const retType = isPrivateCall(functionData.functionType) ? `-> [Field; RETURN_VALUES_LENGTH] ` : ``; + + return ` + fn ${name}( + ${allParams.join(',\n ')} + ) ${retType}{ +${serialisation} +${callStatement} + } + `; +} + +/** + * Generates static impots. + * @returns A string of code which will be needed in every contract interface, regardless of the contract. + */ +function generateStaticImports() { + return `use dep::std; +use dep::aztec::context::Context; +use dep::aztec::constants_gen::RETURN_VALUES_LENGTH;`; +} + +/** + * Generate the main focus of this code generator: the contract interface struct. + * @param contractName - the name of the contract, as matches the original source file. + * @returns Code. + */ +function generateContractInterfaceStruct(contractName: string) { + return `struct ${contractName}ContractInterface { + address: Field, +} +`; +} + +/** + * Generates the implementation of the contract interface struct. + * @param contractName - The name of the contract, as matches the original source file. + * @param functions - An array of strings, where each string is valid Noir code describing the function interface of one of the contract's functions (as generated via `generateFunctionInterface` above). + * @returns Code. + */ +function generateContractInterfaceImpl(contractName: string, functions: string[]) { + return `impl ${contractName}ContractInterface { + fn at(address: Field) -> Self { + Self { + address, + } + } + ${functions.join('\n')} +} +`; +} + +/** Represents a struct along its parent names to derive a fully qualified name. */ +type StructInfo = ABIVariable & { /** Parent name */ parentNames: string[] }; + +/** + * Generates a Noir struct. + * @param struct - Struct info. + * @returns Code representing the struct. + */ +function generateStruct(struct: StructInfo) { + const fields = (struct.type as StructType).fields.map( + field => ` ${field.name}: ${getTypeName(field, struct.name, ...struct.parentNames)},`, + ); + + return ` +struct ${getStructName(struct.name, ...struct.parentNames)} { +${fields.join('\n')} +}`; +} + +/** + * Collects all structs across all parameters. + * @param params - Parameters to look for structs, either structs themselves or nested. + * @param parentNames - Parent names to derive fully qualified names when needed. + * @returns A list of struct infos. + */ +function collectStructs(params: ABIVariable[], parentNames: string[]): StructInfo[] { + const structs: StructInfo[] = []; + for (const param of params) { + if (param.type.kind === 'struct') { + const struct = { ...param, parentNames }; + structs.push(struct, ...collectStructs(param.type.fields, [param.name, ...parentNames])); + } else if (param.type.kind === 'array') { + structs.push(...collectStructs([{ name: param.name, type: param.type.type }], [...parentNames])); + } + } + return structs; +} + +/** + * Generates the Noir code to represent an interface for calling a contract. + * @param abi - The compiled Noir artifact. + * @returns The corresponding ts code. + */ +export function generateNoirContractInterface(abi: ContractAbi) { + // We don't allow calling a constructor, internal fns, or unconstrained fns from other contracts + const methods = compact( + abi.functions.filter( + f => f.name !== 'constructor' && !f.isInternal && f.functionType !== FunctionType.UNCONSTRAINED, + ), + ); + const contractStruct: string = generateContractInterfaceStruct(abi.name); + const paramStructs = methods.flatMap(m => collectStructs(m.parameters, [m.name])).map(generateStruct); + const functionInterfaces = methods.map(generateFunctionInterface); + const contractImpl: string = generateContractInterfaceImpl(abi.name, functionInterfaces); + + return `/* Autogenerated file, do not edit! */ + +${generateStaticImports()} +${paramStructs.join('\n')} + +${contractStruct} +${contractImpl} +`; +} diff --git a/yarn-project/noir-compiler/src/typegen/index.ts b/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts similarity index 97% rename from yarn-project/noir-compiler/src/typegen/index.ts rename to yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts index 74778bc00c2..d3a1ffc6606 100644 --- a/yarn-project/noir-compiler/src/typegen/index.ts +++ b/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts @@ -47,6 +47,33 @@ function generateMethod(entry: FunctionAbi) { ${entry.name}: ((${args}) => ContractFunctionInteraction) & Pick;`; } +/** + * Generates a deploy method for this contract. + * @param input - ABI of the contract. + * @returns A type-safe deploy method in ts. + */ +function generateDeploy(input: ContractAbi) { + const ctor = input.functions.find(f => f.name === 'constructor'); + const args = (ctor?.parameters ?? []).map(generateParameter).join(', '); + const abiName = `${input.name}ContractAbi`; + + return ` + /** + * Creates a tx to deploy a new instance of this contract. + */ + public static deploy(rpc: AztecRPC, ${args}) { + return new DeployMethod<${input.name}Contract>(Point.ZERO, rpc, ${abiName}, Array.from(arguments).slice(1)); + } + + /** + * Creates a tx to deploy a new instance of this contract using the specified public key to derive the address. + */ + public static deployWithPublicKey(rpc: AztecRPC, publicKey: PublicKey, ${args}) { + return new DeployMethod<${input.name}Contract>(publicKey, rpc, ${abiName}, Array.from(arguments).slice(2)); + } + `; +} + /** * Generates the constructor by supplying the ABI to the parent class so the user doesn't have to. * @param name - Name of the contract to derive the ABI name from. @@ -93,33 +120,6 @@ function generateCreate(name: string) { }`; } -/** - * Generates a deploy method for this contract. - * @param input - ABI of the contract. - * @returns A type-safe deploy method in ts. - */ -function generateDeploy(input: ContractAbi) { - const ctor = input.functions.find(f => f.name === 'constructor'); - const args = (ctor?.parameters ?? []).map(generateParameter).join(', '); - const abiName = `${input.name}ContractAbi`; - - return ` - /** - * Creates a tx to deploy a new instance of this contract. - */ - public static deploy(rpc: AztecRPC, ${args}) { - return new DeployMethod<${input.name}Contract>(Point.ZERO, rpc, ${abiName}, Array.from(arguments).slice(1)); - } - - /** - * Creates a tx to deploy a new instance of this contract using the specified public key to derive the address. - */ - public static deployWithPublicKey(rpc: AztecRPC, publicKey: PublicKey, ${args}) { - return new DeployMethod<${input.name}Contract>(publicKey, rpc, ${abiName}, Array.from(arguments).slice(2)); - } - `; -} - /** * Generates a static getter for the contract's ABI. * @param name - Name of the contract used to derive name of the ABI import. @@ -142,7 +142,8 @@ function generateAbiGetter(name: string) { * @param abiImportPath - Optional path to import the ABI (if not set, will be required in the constructor). * @returns The corresponding ts code. */ -export function generateType(input: ContractAbi, abiImportPath?: string) { +export function generateTSContractInterface(input: ContractAbi, abiImportPath?: string) { + // `compact` removes all falsey values from an array const methods = compact(input.functions.filter(f => f.name !== 'constructor').map(generateMethod)); const deploy = abiImportPath && generateDeploy(input); const ctor = abiImportPath && generateConstructor(input.name); diff --git a/yarn-project/noir-compiler/src/index.ts b/yarn-project/noir-compiler/src/index.ts index e5d5e24730a..f3dcb1fccab 100644 --- a/yarn-project/noir-compiler/src/index.ts +++ b/yarn-project/noir-compiler/src/index.ts @@ -1,2 +1,3 @@ export * from './compile.js'; -export * from './typegen/index.js'; +export * from './contract-interface-gen/typescript.js'; +export * from './contract-interface-gen/noir.js'; diff --git a/yarn-project/noir-contracts/scripts/types.sh b/yarn-project/noir-contracts/scripts/types.sh index 6ba44fc1b02..c99b99e8f92 100755 --- a/yarn-project/noir-contracts/scripts/types.sh +++ b/yarn-project/noir-contracts/scripts/types.sh @@ -22,7 +22,8 @@ ROOT=$(pwd) write_import() { CONTRACT_NAME=$1 - + + # Convert to PascalCase if [ "$(uname)" = "Darwin" ]; then # sed \U doesn't work on mac NAME=$(echo $CONTRACT_NAME | perl -pe 's/(^|_)(\w)/\U$2/g') @@ -35,6 +36,8 @@ write_import() { write_export() { CONTRACT_NAME=$1 + + # Convert to PascalCase if [ "$(uname)" = "Darwin" ]; then # sed \U doesn't work on mac NAME=$(echo $CONTRACT_NAME | perl -pe 's/(^|_)(\w)/\U$2/g') diff --git a/yarn-project/noir-contracts/src/contracts/child_contract/src/storage.nr b/yarn-project/noir-contracts/src/contracts/child_contract/src/storage.nr index 634674859e4..5618cf75889 100644 --- a/yarn-project/noir-contracts/src/contracts/child_contract/src/storage.nr +++ b/yarn-project/noir-contracts/src/contracts/child_contract/src/storage.nr @@ -1,6 +1,6 @@ use dep::aztec::state_vars::public_state::PublicState; -use dep::aztec::state_vars::type_serialisation::field_serialisation::FieldSerialisationMethods; -use dep::aztec::state_vars::type_serialisation::field_serialisation::FIELD_SERIALISED_LEN; +use dep::aztec::types::type_serialisation::field_serialisation::FieldSerialisationMethods; +use dep::aztec::types::type_serialisation::field_serialisation::FIELD_SERIALISED_LEN; struct Storage { current_value: PublicState, diff --git a/yarn-project/noir-contracts/src/contracts/import_test_contract/Nargo.toml b/yarn-project/noir-contracts/src/contracts/import_test_contract/Nargo.toml new file mode 100644 index 00000000000..41b131fcc79 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/import_test_contract/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "import_test_contract" +authors = [""] +compiler_version = "0.1" +type = "bin" + +[dependencies] +aztec = { path = "../../../../noir-libs/noir-aztec" } \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/import_test_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/import_test_contract/src/main.nr new file mode 100644 index 00000000000..2eb0637b369 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/import_test_contract/src/main.nr @@ -0,0 +1,84 @@ +mod test_contract_interface; + +// Contract that uses the autogenerated interface of the Test contract for calling its functions. +// Used for testing calling into other contracts via autogenerated interfaces. +contract ImportTest { + use dep::aztec::abi; + use dep::aztec::abi::PrivateContextInputs; + use dep::aztec::context::Context; + + use crate::test_contract_interface::{ + TestContractInterface, + AStructTestCodeGenStruct, + ADeepStructTestCodeGenStruct, + ANoteADeepStructTestCodeGenStruct, + ManyNotesADeepStructTestCodeGenStruct, + }; + + fn constructor( + inputs: PrivateContextInputs, + ) -> distinct pub abi::PrivateCircuitPublicInputs { + Context::new(inputs, 0).finish() + } + + // Calls the testCodeGen on the Test contract at the target address + // Used for testing calling a function with arguments of multiple types + // See yarn-project/acir-simulator/src/client/private_execution.ts + // See yarn-project/end-to-end/src/e2e_nested_contract.test.ts + fn main( + inputs: PrivateContextInputs, + target: Field + ) -> distinct pub abi::PrivateCircuitPublicInputs { + let mut context = Context::new(inputs, abi::hash_args([target])); + let test_contract_instance = TestContractInterface::at(target); + let return_values = test_contract_instance.testCodeGen( + &mut context, + 1, + true, + 1 as u32, + [1, 2], + AStructTestCodeGenStruct { amount: 1, secretHash: 2 }, + ADeepStructTestCodeGenStruct { + aField: 1, + aBool: true, + aNote: ANoteADeepStructTestCodeGenStruct { amount: 1, secretHash: 2 }, + manyNotes: [ + ManyNotesADeepStructTestCodeGenStruct { amount: 1, secretHash: 2 }, + ManyNotesADeepStructTestCodeGenStruct { amount: 1, secretHash: 2 }, + ManyNotesADeepStructTestCodeGenStruct { amount: 1, secretHash: 2 }, + ] + } + ); + + context.return_values.push(return_values[0]); + context.finish() + } + + // Calls the getThisAddress on the Test contract at the target address + // Used for testing calling a function with no arguments + // See yarn-project/end-to-end/src/e2e_nested_contract.test.ts + fn callNoArgs( + inputs: PrivateContextInputs, + target: Field + ) -> distinct pub abi::PrivateCircuitPublicInputs { + let mut context = Context::new(inputs, abi::hash_args([target])); + let test_contract_instance = TestContractInterface::at(target); + let return_values = test_contract_instance.getThisAddress(&mut context); + context.return_values.push(return_values[0]); + context.finish() + } + + // Calls the createNullifierPublic on the Test contract at the target address + // Used for testing calling an open function + // See yarn-project/end-to-end/src/e2e_nested_contract.test.ts + fn callOpenFn( + inputs: PrivateContextInputs, + target: Field, + ) -> distinct pub abi::PrivateCircuitPublicInputs { + let mut context = Context::new(inputs, abi::hash_args([target])); + let test_contract_instance = TestContractInterface::at(target); + test_contract_instance.createNullifierPublic(&mut context, 1, 2); + context.finish() + } +} + diff --git a/yarn-project/noir-contracts/src/contracts/import_test_contract/src/test_contract_interface.nr b/yarn-project/noir-contracts/src/contracts/import_test_contract/src/test_contract_interface.nr new file mode 120000 index 00000000000..412fbaacd2d --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/import_test_contract/src/test_contract_interface.nr @@ -0,0 +1 @@ +../../test_contract/src/test_contract_interface.nr \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/lending_contract/src/storage.nr b/yarn-project/noir-contracts/src/contracts/lending_contract/src/storage.nr index 4f247028da6..61f021e06a7 100644 --- a/yarn-project/noir-contracts/src/contracts/lending_contract/src/storage.nr +++ b/yarn-project/noir-contracts/src/contracts/lending_contract/src/storage.nr @@ -1,8 +1,8 @@ use dep::aztec::state_vars::map::Map; use dep::aztec::state_vars::public_state::PublicState; -use dep::aztec::state_vars::type_serialisation::TypeSerialisationInterface; -use dep::aztec::state_vars::type_serialisation::field_serialisation::FieldSerialisationMethods; -use dep::aztec::state_vars::type_serialisation::field_serialisation::FIELD_SERIALISED_LEN; +use dep::aztec::types::type_serialisation::TypeSerialisationInterface; +use dep::aztec::types::type_serialisation::field_serialisation::FieldSerialisationMethods; +use dep::aztec::types::type_serialisation::field_serialisation::FIELD_SERIALISED_LEN; use dep::std::hash::pedersen; // Utility struct used to easily get a "id" for a private user that sits in the same diff --git a/yarn-project/noir-contracts/src/contracts/non_native_token_contract/src/storage.nr b/yarn-project/noir-contracts/src/contracts/non_native_token_contract/src/storage.nr index 5b5e0329dd6..b1cc0d49517 100644 --- a/yarn-project/noir-contracts/src/contracts/non_native_token_contract/src/storage.nr +++ b/yarn-project/noir-contracts/src/contracts/non_native_token_contract/src/storage.nr @@ -12,13 +12,17 @@ use dep::value_note::value_note::{ VALUE_NOTE_LEN, }; -use dep::aztec::state_vars::{ - map::Map, - set::Set, - public_state::PublicState, +use dep::aztec::{ + state_vars::{ + map::Map, + set::Set, + public_state::PublicState, + }, + types::type_serialisation::field_serialisation::{ + FIELD_SERIALISED_LEN, + FieldSerialisationMethods, + }, }; -use dep::aztec::state_vars::type_serialisation::field_serialisation::FieldSerialisationMethods; -use dep::aztec::state_vars::type_serialisation::field_serialisation::FIELD_SERIALISED_LEN; struct Storage { balances: Map>, diff --git a/yarn-project/noir-contracts/src/contracts/parent_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/parent_contract/src/main.nr index b78fd50d568..9c3dca33ed7 100644 --- a/yarn-project/noir-contracts/src/contracts/parent_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/parent_contract/src/main.nr @@ -58,7 +58,7 @@ contract Parent { targetValue, ])); - let _callStackItem = context.call_public_function(targetContract, targetSelector, [targetValue]); + context.call_public_function(targetContract, targetSelector, [targetValue]); // Return private circuit public inputs. All private functions need to return this as it is part of the input of the private kernel. context.finish() @@ -78,9 +78,9 @@ contract Parent { ])); // Enqueue the first public call - let return_values1 = context.call_public_function(targetContract, targetSelector, [targetValue]); + context.call_public_function(targetContract, targetSelector, [targetValue]); // Enqueue the second public call - let _return_values2 = context.call_public_function(targetContract, targetSelector, [return_values1[0]]); + context.call_public_function(targetContract, targetSelector, [targetValue]); // Return private circuit public inputs. All private functions need to return this as it is part of the input of the private kernel. context.finish() @@ -101,7 +101,7 @@ contract Parent { let pubEntryPointSelector = 3221316504; let thisAddress = inputs.call_context.storage_contract_address; - let _return_values = context.call_public_function(thisAddress, pubEntryPointSelector, [targetContract, targetSelector, targetValue]); + context.call_public_function(thisAddress, pubEntryPointSelector, [targetContract, targetSelector, targetValue]); // Return private circuit public inputs. All private functions need to return this as it is part of the input of the private kernel. context.finish() @@ -123,9 +123,9 @@ contract Parent { let pubEntryPointSelector = 3221316504; let thisAddress = inputs.call_context.storage_contract_address; - let _return_values1 = context.call_public_function(thisAddress, pubEntryPointSelector, [targetContract, targetSelector, targetValue]); + context.call_public_function(thisAddress, pubEntryPointSelector, [targetContract, targetSelector, targetValue]); - let _return_values2 = context.call_public_function(thisAddress, pubEntryPointSelector, [targetContract, targetSelector, targetValue + 1]); + context.call_public_function(thisAddress, pubEntryPointSelector, [targetContract, targetSelector, targetValue + 1]); // Return private circuit public inputs. All private functions need to return this as it is part of the input of the private kernel. context.finish() diff --git a/yarn-project/noir-contracts/src/contracts/public_token_contract/src/storage.nr b/yarn-project/noir-contracts/src/contracts/public_token_contract/src/storage.nr index f5c2a4f61ce..9513325c9fa 100644 --- a/yarn-project/noir-contracts/src/contracts/public_token_contract/src/storage.nr +++ b/yarn-project/noir-contracts/src/contracts/public_token_contract/src/storage.nr @@ -3,13 +3,14 @@ use dep::aztec::state_vars::{ map::Map, // highlight-start:PublicState public_state::PublicState, - type_serialisation::field_serialisation::{ - FieldSerialisationMethods, - FIELD_SERIALISED_LEN, - }, // highlight-end:PublicState }; +use dep::aztec::types::type_serialisation::field_serialisation::{ + FieldSerialisationMethods, + FIELD_SERIALISED_LEN, +}; + struct Storage { // highlight-next-line:PublicState balances: Map>, diff --git a/yarn-project/noir-contracts/src/contracts/test_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/test_contract/src/main.nr index db44cc275b5..33c03660f08 100644 --- a/yarn-project/noir-contracts/src/contracts/test_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/test_contract/src/main.nr @@ -1,7 +1,8 @@ -// A contract used to test whether constructing a contract works. +// A contract used for testing a random hodgepodge of small features from simulator and end-to-end tests. contract Test { use dep::aztec::{ abi, + types::vec::BoundedVec, abi::{ PublicContextInputs, PrivateContextInputs @@ -13,7 +14,8 @@ contract Test { create_l2_to_l1_message::create_l2_to_l1_message, create_nullifier::create_nullifier, get_public_key::get_public_key, - context::get_portal_address + context::get_portal_address, + rand::rand, }; fn constructor( @@ -64,6 +66,38 @@ contract Test { context.finish() } + // Test codegen for noir interfaces + // See yarn-project/acir-simulator/src/client/private_execution.test.ts 'nested calls through autogenerated interface' + fn testCodeGen( + inputs: PrivateContextInputs, + aField: Field, + aBool: bool, + aNumber: u32, + anArray: [Field; 2], + aStruct: DummyNote, + aDeepStruct: DeepStruct, + ) -> distinct pub abi::PrivateCircuitPublicInputs { + let mut args: BoundedVec = BoundedVec::new(0); + args.push(aField); + args.push(aBool as Field); + args.push(aNumber as Field); + args.push_array(anArray); + args.push(aStruct.amount); + args.push(aStruct.secretHash); + args.push(aDeepStruct.aField); + args.push(aDeepStruct.aBool as Field); + args.push(aDeepStruct.aNote.amount); + args.push(aDeepStruct.aNote.secretHash); + for note in aDeepStruct.manyNotes { + args.push(note.amount); + args.push(note.secretHash); + } + let args_hash = abi::hash_args(args.storage); + let mut context = Context::new(inputs, args_hash); + context.return_values.push(args_hash); + context.finish() + } + // Purely exists for testing open fn createL2ToL1MessagePublic( _inputs: PublicContextInputs, @@ -92,6 +126,13 @@ contract Test { 0 } + // Purely exists for testing + unconstrained fn getRandom( + kindaSeed: Field + ) -> Field { + kindaSeed * rand() + } + struct DummyNote { amount: Field, secretHash: Field @@ -109,4 +150,11 @@ contract Test { dep::std::hash::pedersen([self.amount, self.secretHash])[0] } } + + struct DeepStruct { + aField: Field, + aBool: bool, + aNote: DummyNote, + manyNotes: [DummyNote; 3], + } } diff --git a/yarn-project/noir-contracts/src/contracts/test_contract/src/test_contract_interface.nr b/yarn-project/noir-contracts/src/contracts/test_contract/src/test_contract_interface.nr new file mode 100644 index 00000000000..32603d90544 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/test_contract/src/test_contract_interface.nr @@ -0,0 +1,145 @@ +/* Autogenerated file, do not edit! */ + +use dep::std; +use dep::aztec::context::Context; +use dep::aztec::constants_gen::RETURN_VALUES_LENGTH; + +struct AStructTestCodeGenStruct { + amount: Field, + secretHash: Field, +} + +struct ADeepStructTestCodeGenStruct { + aField: Field, + aBool: bool, + aNote: ANoteADeepStructTestCodeGenStruct, + manyNotes: [ManyNotesADeepStructTestCodeGenStruct;3], +} + +struct ANoteADeepStructTestCodeGenStruct { + amount: Field, + secretHash: Field, +} + +struct ManyNotesADeepStructTestCodeGenStruct { + amount: Field, + secretHash: Field, +} + +struct TestContractInterface { + address: Field, +} + +impl TestContractInterface { + fn at(address: Field) -> Self { + Self { + address, + } + } + + fn createL2ToL1MessagePublic( + self, + context: &mut Context, + amount: Field, + secretHash: Field + ) { + let mut serialised_args = [0; 2]; + serialised_args[0] = amount; + serialised_args[1] = secretHash; + + context.call_public_function(self.address, 0x1c031d17, serialised_args) + } + + + fn createNullifierPublic( + self, + context: &mut Context, + amount: Field, + secretHash: Field + ) { + let mut serialised_args = [0; 2]; + serialised_args[0] = amount; + serialised_args[1] = secretHash; + + context.call_public_function(self.address, 0x0217ef40, serialised_args) + } + + + fn getPortalContractAddress( + self, + context: &mut Context, + aztec_address: Field + ) -> [Field; RETURN_VALUES_LENGTH] { + let mut serialised_args = [0; 1]; + serialised_args[0] = aztec_address; + + context.call_private_function(self.address, 0xe5df1726, serialised_args) + } + + + fn getPublicKey( + self, + context: &mut Context, + address: Field + ) -> [Field; RETURN_VALUES_LENGTH] { + let mut serialised_args = [0; 1]; + serialised_args[0] = address; + + context.call_private_function(self.address, 0x553aaad4, serialised_args) + } + + + fn getThisAddress( + self, + context: &mut Context + ) -> [Field; RETURN_VALUES_LENGTH] { + let mut serialised_args = [0; 0]; + + context.call_private_function(self.address, 0xd3953822, serialised_args) + } + + + fn getThisPortalAddress( + self, + context: &mut Context + ) -> [Field; RETURN_VALUES_LENGTH] { + let mut serialised_args = [0; 0]; + + context.call_private_function(self.address, 0x82cc9431, serialised_args) + } + + + fn testCodeGen( + self, + context: &mut Context, + aField: Field, + aBool: bool, + aNumber: u32, + anArray: [Field;2], + aStruct: AStructTestCodeGenStruct, + aDeepStruct: ADeepStructTestCodeGenStruct + ) -> [Field; RETURN_VALUES_LENGTH] { + let mut serialised_args = [0; 17]; + serialised_args[0] = aField; + serialised_args[1] = aBool as Field; + serialised_args[2] = aNumber as Field; + serialised_args[3] = anArray[0]; + serialised_args[4] = anArray[1]; + serialised_args[5] = aStruct.amount; + serialised_args[6] = aStruct.secretHash; + serialised_args[7] = aDeepStruct.aField; + serialised_args[8] = aDeepStruct.aBool as Field; + serialised_args[9] = aDeepStruct.aNote.amount; + serialised_args[10] = aDeepStruct.aNote.secretHash; + serialised_args[11] = aDeepStruct.manyNotes[0].amount; + serialised_args[12] = aDeepStruct.manyNotes[0].secretHash; + serialised_args[13] = aDeepStruct.manyNotes[1].amount; + serialised_args[14] = aDeepStruct.manyNotes[1].secretHash; + serialised_args[15] = aDeepStruct.manyNotes[2].amount; + serialised_args[16] = aDeepStruct.manyNotes[2].secretHash; + + context.call_private_function(self.address, 0x7c97ca29, serialised_args) + } + +} + diff --git a/yarn-project/noir-contracts/src/scripts/copy_output.ts b/yarn-project/noir-contracts/src/scripts/copy_output.ts index 7e951210ee4..3531b916e41 100644 --- a/yarn-project/noir-contracts/src/scripts/copy_output.ts +++ b/yarn-project/noir-contracts/src/scripts/copy_output.ts @@ -1,6 +1,6 @@ -import { ABIParameter, ABIType, FunctionType } from '@aztec/foundation/abi'; +import { ContractAbi, FunctionAbi, FunctionType } from '@aztec/foundation/abi'; import { createConsoleLogger } from '@aztec/foundation/log'; -import { generateType } from '@aztec/noir-compiler'; +import { generateNoirContractInterface, generateTSContractInterface } from '@aztec/noir-compiler'; import { readFileSync, writeFileSync } from 'fs'; import camelCase from 'lodash.camelcase'; @@ -11,7 +11,7 @@ import { join as pathJoin } from 'path'; import mockedKeys from './mockedKeys.json' assert { type: 'json' }; -const STATEMENT_TYPES = ['type', 'params', 'return'] as const; +// const STATEMENT_TYPES = ['type', 'params', 'return'] as const; const log = createConsoleLogger('aztec:noir-contracts'); const PROJECT_CONTRACTS = [ @@ -20,6 +20,8 @@ const PROJECT_CONTRACTS = [ { name: 'EcdsaAccount', target: '../aztec.js/src/abis/', exclude: [] }, ]; +const INTERFACE_CONTRACTS = ['test']; + /** * Writes the contract to a specific project folder, if needed. * @param abi - The Abi to write. @@ -40,27 +42,28 @@ function writeToProject(abi: any) { /** * Creates an Aztec function entry. - * @param type - The type of the function. - * @param params - The parameters of the function. + * @param type - The type of the function (secret | open | unconstrained). + * @param params - The parameters of the function ( name, type, visibility ). * @param returns - The return types of the function. * @param fn - The nargo function entry. * @returns The Aztec function entry. */ -function getFunction(type: FunctionType, params: ABIParameter[], returns: ABIType[], fn: any) { - if (!params) throw new Error(`ABI comment not found for function ${fn.name}`); +function getFunction(fn: any): FunctionAbi { + const type = fn.function_type.toLowerCase(); + const returns = fn.abi.return_type; + const isInternal = fn.is_internal; + let params = fn.abi.parameters; + // If the function is not unconstrained, the first item is inputs or CallContext which we should omit if (type !== FunctionType.UNCONSTRAINED) params = params.slice(1); - // If the function is not secret, drop any padding from the end - if (type !== FunctionType.SECRET && params.length > 0 && params[params.length - 1].name.endsWith('padding')) - params = params.slice(0, params.length - 1); return { name: fn.name, functionType: type, - isInternal: fn.is_internal, + isInternal, parameters: params, // If the function is secret, the return is the public inputs, which should be omitted - returnTypes: type === FunctionType.SECRET ? [] : returns, + returnTypes: type === FunctionType.SECRET ? [] : [returns], bytecode: fn.bytecode, // verificationKey: Buffer.from(fn.verification_key).toString('hex'), verificationKey: mockedKeys.verificationKey, @@ -69,36 +72,21 @@ function getFunction(type: FunctionType, params: ABIParameter[], returns: ABITyp /** * Creates the Aztec function entries from the source code and the nargo output. - * @param source - The source code of the contract. - * @param output - The nargo output. + * @param sourceCode - The source code of the contract. + * @param buildJson - The nargo output. * @returns The Aztec function entries. */ -function getFunctions(source: string, output: any) { - const abiComments = Array.from(source.matchAll(/\/\/\/ ABI (\w+) (params|return|type) (.+)/g)).map(match => ({ - functionName: match[1], - statementType: match[2], - value: JSON.parse(match[3]), - })); - - return output.functions +function getFunctions(_sourceCode: string, buildJson: any): FunctionAbi[] { + /** + * Sort functions alphabetically, by name. + * Remove the proving key field of the function. + * + */ + return buildJson.functions .sort((fnA: any, fnB: any) => fnA.name.localeCompare(fnB.name)) .map((fn: any) => { delete fn.proving_key; - const thisFunctionAbisComments = abiComments - .filter(abi => abi.functionName === fn.name) - .reduce( - (acc, comment) => ({ - ...acc, - [comment.statementType]: comment.value, - }), - {} as Record<(typeof STATEMENT_TYPES)[number], any>, - ); - return getFunction( - thisFunctionAbisComments.type || (fn.function_type.toLowerCase() as FunctionType), - thisFunctionAbisComments.params || fn.abi.parameters, - thisFunctionAbisComments.return || [fn.abi.return_type], - fn, - ); + return getFunction(fn); }); } @@ -106,27 +94,45 @@ const main = () => { const name = process.argv[2]; if (!name) throw new Error(`Missing argument contract name`); - const folderName = `${snakeCase(name)}_contract`; - const folderPath = `src/contracts/${folderName}`; - const source = readFileSync(`${folderPath}/src/main.nr`).toString(); - const contractName = process.argv[3] ?? upperFirst(camelCase(name)); - const build = JSON.parse(readFileSync(`${folderPath}/target/${folderName}-${contractName}.json`).toString()); - const artifacts = 'src/artifacts'; - - const abi = { - name: build.name, - functions: getFunctions(source, build), - }; + const projectName = `${snakeCase(name)}_contract`; + const projectDirPath = `src/contracts/${projectName}`; + const sourceCodeFilePath = `${projectDirPath}/src/main.nr`; + const sourceCode = readFileSync(sourceCodeFilePath).toString(); - const exampleFile = `${artifacts}/${snakeCase(name)}_contract.json`; - writeFileSync(exampleFile, JSON.stringify(abi, null, 2) + '\n'); - log(`Written ${exampleFile}`); + const contractName = upperFirst(camelCase(name)); + const buildJsonFilePath = `${projectDirPath}/target/${projectName}-${contractName}.json`; + const buildJson = JSON.parse(readFileSync(buildJsonFilePath).toString()); - writeToProject(abi); + // Remove extraneous information from the buildJson (which was output by Nargo) to hone in on the function data we actually care about: + const artifactJson: ContractAbi = { + name: buildJson.name, + functions: getFunctions(sourceCode, buildJson), + }; - const typeFile = `src/types/${name}.ts`; - writeFileSync(typeFile, generateType(abi, '../artifacts/index.js')); - log(`Written ${typeFile}`); + // Write the artifact: + const artifactsDir = 'src/artifacts'; + const artifactDestFilePath = `${artifactsDir}/${snakeCase(name)}_contract.json`; + writeFileSync(artifactDestFilePath, JSON.stringify(artifactJson, null, 2) + '\n'); + log(`Written ${artifactDestFilePath}`); + + // Write some artifacts to other packages in the monorepo: + writeToProject(artifactJson); + + // Write a .ts contract interface, for consumption by the typescript code + const tsInterfaceDestFilePath = `src/types/${name}.ts`; + writeFileSync(tsInterfaceDestFilePath, generateTSContractInterface(artifactJson, '../artifacts/index.js')); + log(`Written ${tsInterfaceDestFilePath}`); + + // Write a .nr contract interface, for consumption by other Noir Contracts + if (INTERFACE_CONTRACTS.includes(name)) { + const noirInterfaceDestFilePath = `${projectDirPath}/src/${projectName}_interface.nr`; + try { + writeFileSync(noirInterfaceDestFilePath, generateNoirContractInterface(artifactJson)); + log(`Written ${noirInterfaceDestFilePath}`); + } catch (err) { + log(`Error generating noir interface for ${name}: ${err}`); + } + } }; try { diff --git a/yarn-project/noir-libs/noir-aztec/src/abi.nr b/yarn-project/noir-libs/noir-aztec/src/abi.nr index b2a4240db29..be4fbfc8da4 100644 --- a/yarn-project/noir-libs/noir-aztec/src/abi.nr +++ b/yarn-project/noir-libs/noir-aztec/src/abi.nr @@ -358,21 +358,25 @@ global ARGS_HASH_CHUNK_LENGTH: u32 = 32; global ARGS_HASH_CHUNK_COUNT: u32 = 16; fn hash_args(args: [Field; N]) -> Field { - let mut chunks_hashes = [0; ARGS_HASH_CHUNK_COUNT]; - for i in 0..ARGS_HASH_CHUNK_COUNT { - let mut chunk_hash = 0; - let start_chunk_index = i * ARGS_HASH_CHUNK_LENGTH; - if start_chunk_index < (args.len() as u32) { - let mut chunk_args = [0; ARGS_HASH_CHUNK_LENGTH]; - for j in 0..ARGS_HASH_CHUNK_LENGTH { - let item_index = i * ARGS_HASH_CHUNK_LENGTH + j; - if item_index < (args.len() as u32) { - chunk_args[j] = args[item_index]; + if args.len() == 0 { + 0 + } else { + let mut chunks_hashes = [0; ARGS_HASH_CHUNK_COUNT]; + for i in 0..ARGS_HASH_CHUNK_COUNT { + let mut chunk_hash = 0; + let start_chunk_index = i * ARGS_HASH_CHUNK_LENGTH; + if start_chunk_index < (args.len() as u32) { + let mut chunk_args = [0; ARGS_HASH_CHUNK_LENGTH]; + for j in 0..ARGS_HASH_CHUNK_LENGTH { + let item_index = i * ARGS_HASH_CHUNK_LENGTH + j; + if item_index < (args.len() as u32) { + chunk_args[j] = args[item_index]; + } } + chunk_hash = dep::std::hash::pedersen_with_separator(chunk_args, GENERATOR_INDEX__FUNCTION_ARGS)[0]; } - chunk_hash = dep::std::hash::pedersen_with_separator(chunk_args, GENERATOR_INDEX__FUNCTION_ARGS)[0]; + chunks_hashes[i] = chunk_hash; } - chunks_hashes[i] = chunk_hash; + dep::std::hash::pedersen_with_separator(chunks_hashes, GENERATOR_INDEX__FUNCTION_ARGS)[0] } - dep::std::hash::pedersen_with_separator(chunks_hashes, GENERATOR_INDEX__FUNCTION_ARGS)[0] } diff --git a/yarn-project/noir-libs/noir-aztec/src/context.nr b/yarn-project/noir-libs/noir-aztec/src/context.nr index 8c20f03f1a0..82c7b90f96d 100644 --- a/yarn-project/noir-libs/noir-aztec/src/context.nr +++ b/yarn-project/noir-libs/noir-aztec/src/context.nr @@ -296,7 +296,7 @@ impl Context { contract_address: Field, function_selector: Field, args: [Field; ARGS_COUNT] - ) -> [Field; RETURN_VALUES_LENGTH] { + ) { let args_hash = hash_args(args); assert(args_hash == arguments::pack_arguments(args)); self.call_public_function_with_packed_args(contract_address, function_selector, args_hash) @@ -306,7 +306,7 @@ impl Context { &mut self, contract_address: Field, function_selector: Field, - ) -> [Field; RETURN_VALUES_LENGTH] { + ) { self.call_public_function_with_packed_args(contract_address, function_selector, 0) } @@ -315,7 +315,7 @@ impl Context { contract_address: Field, function_selector: Field, args_hash: Field - ) -> [Field; RETURN_VALUES_LENGTH] { + ) { let fields = enqueue_public_function_call_internal( contract_address, function_selector, @@ -372,7 +372,5 @@ impl Context { assert(item.public_inputs.call_context.storage_contract_address == contract_address); self.public_call_stack.push(item.hash()); - - item.public_inputs.return_values } } diff --git a/yarn-project/noir-libs/noir-aztec/src/state_vars.nr b/yarn-project/noir-libs/noir-aztec/src/state_vars.nr index 0148c8a5459..b35df0399d1 100644 --- a/yarn-project/noir-libs/noir-aztec/src/state_vars.nr +++ b/yarn-project/noir-libs/noir-aztec/src/state_vars.nr @@ -1,6 +1,5 @@ mod immutable_singleton; mod map; mod public_state; -mod type_serialisation; mod set; mod singleton; \ No newline at end of file diff --git a/yarn-project/noir-libs/noir-aztec/src/state_vars/public_state.nr b/yarn-project/noir-libs/noir-aztec/src/state_vars/public_state.nr index b4cae24ecfa..900255b5625 100644 --- a/yarn-project/noir-libs/noir-aztec/src/state_vars/public_state.nr +++ b/yarn-project/noir-libs/noir-aztec/src/state_vars/public_state.nr @@ -1,6 +1,6 @@ use crate::oracle::storage::storage_read; use crate::oracle::storage::storage_write; -use crate::state_vars::type_serialisation::TypeSerialisationInterface; +use crate::types::type_serialisation::TypeSerialisationInterface; struct PublicState { storage_slot: Field, diff --git a/yarn-project/noir-libs/noir-aztec/src/types.nr b/yarn-project/noir-libs/noir-aztec/src/types.nr index 4614a0c05bd..babc0a6298c 100644 --- a/yarn-project/noir-libs/noir-aztec/src/types.nr +++ b/yarn-project/noir-libs/noir-aztec/src/types.nr @@ -1,3 +1,4 @@ +mod option; // This can/should be moved out into an official noir library mod point; mod vec; // This can/should be moved out into an official noir library -mod option; // This can/should be moved out into an official noir library \ No newline at end of file +mod type_serialisation; \ No newline at end of file diff --git a/yarn-project/noir-libs/noir-aztec/src/types/point.nr b/yarn-project/noir-libs/noir-aztec/src/types/point.nr index 81e48422671..47ea73ffcde 100644 --- a/yarn-project/noir-libs/noir-aztec/src/types/point.nr +++ b/yarn-project/noir-libs/noir-aztec/src/types/point.nr @@ -1,3 +1,5 @@ +use crate::types::type_serialisation::TypeSerialisationInterface; + struct Point { x: Field, y: Field, @@ -7,4 +9,22 @@ impl Point { fn new(x: Field, y: Field) -> Self { Point { x, y } } -} \ No newline at end of file +} + +global POINT_SERIALISED_LEN: Field = 2; + +fn deserialisePoint(fields: [Field; POINT_SERIALISED_LEN]) -> Point { + Point { + x: fields[0], + y: fields[1], + } +} + +fn serialisePoint(point: Point) -> [Field; POINT_SERIALISED_LEN] { + [point.x, point.y] +} + +global PointSerialisationMethods = TypeSerialisationInterface { + deserialise: deserialisePoint, + serialise: serialisePoint, +}; \ No newline at end of file diff --git a/yarn-project/noir-libs/noir-aztec/src/state_vars/type_serialisation.nr b/yarn-project/noir-libs/noir-aztec/src/types/type_serialisation.nr similarity index 92% rename from yarn-project/noir-libs/noir-aztec/src/state_vars/type_serialisation.nr rename to yarn-project/noir-libs/noir-aztec/src/types/type_serialisation.nr index d76e52102a5..dfe738d2c38 100644 --- a/yarn-project/noir-libs/noir-aztec/src/state_vars/type_serialisation.nr +++ b/yarn-project/noir-libs/noir-aztec/src/types/type_serialisation.nr @@ -1,3 +1,4 @@ +mod bool_serialisation; mod field_serialisation; mod u32_serialisation; diff --git a/yarn-project/noir-libs/noir-aztec/src/types/type_serialisation/bool_serialisation.nr b/yarn-project/noir-libs/noir-aztec/src/types/type_serialisation/bool_serialisation.nr new file mode 100644 index 00000000000..734f725f35b --- /dev/null +++ b/yarn-project/noir-libs/noir-aztec/src/types/type_serialisation/bool_serialisation.nr @@ -0,0 +1,16 @@ +use crate::types::type_serialisation::TypeSerialisationInterface; + +global BOOL_SERIALISED_LEN: Field = 1; + +fn deserialiseBool(fields: [Field; BOOL_SERIALISED_LEN]) -> bool { + fields[0] as bool +} + +fn serialiseBool(value: bool) -> [Field; BOOL_SERIALISED_LEN] { + [value as Field] +} + +global BoolSerialisationMethods = TypeSerialisationInterface { + deserialise: deserialiseBool, + serialise: serialiseBool, +}; \ No newline at end of file diff --git a/yarn-project/noir-libs/noir-aztec/src/state_vars/type_serialisation/field_serialisation.nr b/yarn-project/noir-libs/noir-aztec/src/types/type_serialisation/field_serialisation.nr similarity index 82% rename from yarn-project/noir-libs/noir-aztec/src/state_vars/type_serialisation/field_serialisation.nr rename to yarn-project/noir-libs/noir-aztec/src/types/type_serialisation/field_serialisation.nr index 5352d0e3e5f..5fcaf370523 100644 --- a/yarn-project/noir-libs/noir-aztec/src/state_vars/type_serialisation/field_serialisation.nr +++ b/yarn-project/noir-libs/noir-aztec/src/types/type_serialisation/field_serialisation.nr @@ -1,4 +1,4 @@ -use crate::state_vars::type_serialisation::TypeSerialisationInterface; +use crate::types::type_serialisation::TypeSerialisationInterface; global FIELD_SERIALISED_LEN: Field = 1; diff --git a/yarn-project/noir-libs/noir-aztec/src/state_vars/type_serialisation/u32_serialisation.nr b/yarn-project/noir-libs/noir-aztec/src/types/type_serialisation/u32_serialisation.nr similarity index 82% rename from yarn-project/noir-libs/noir-aztec/src/state_vars/type_serialisation/u32_serialisation.nr rename to yarn-project/noir-libs/noir-aztec/src/types/type_serialisation/u32_serialisation.nr index 21178a167f2..dd00ebfedfd 100644 --- a/yarn-project/noir-libs/noir-aztec/src/state_vars/type_serialisation/u32_serialisation.nr +++ b/yarn-project/noir-libs/noir-aztec/src/types/type_serialisation/u32_serialisation.nr @@ -1,4 +1,4 @@ -use crate::state_vars::type_serialisation::TypeSerialisationInterface; +use crate::types::type_serialisation::TypeSerialisationInterface; global U32_SERIALISED_LEN: Field = 1; diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index cd531e7afa5..841c74cf40d 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -523,12 +523,20 @@ __metadata: "@rushstack/eslint-patch": ^1.1.4 "@types/fs-extra": ^11.0.1 "@types/jest": ^29.5.0 + "@types/lodash.camelcase": ^4.3.7 + "@types/lodash.capitalize": ^4.2.7 "@types/lodash.compact": ^3.0.7 + "@types/lodash.times": ^4.3.7 + "@types/lodash.upperfirst": ^4.3.7 "@types/node": ^18.7.23 commander: ^9.0.0 fs-extra: ^11.1.1 jest: ^29.5.0 + lodash.camelcase: ^4.3.0 + lodash.capitalize: ^4.2.1 lodash.compact: ^3.0.1 + lodash.times: ^4.3.2 + lodash.upperfirst: ^4.3.1 toml: ^3.0.0 ts-jest: ^29.1.0 ts-node: ^10.9.1 @@ -3231,6 +3239,15 @@ __metadata: languageName: node linkType: hard +"@types/lodash.capitalize@npm:^4.2.7": + version: 4.2.7 + resolution: "@types/lodash.capitalize@npm:4.2.7" + dependencies: + "@types/lodash": "*" + checksum: dab8b781d7dcc56c18ba0c8286a6ccb61cc598d936a449265453a473e62b2b6d7c109c4447dfeb8ccacc4088769bc3bfd0d39bc8797f03e4e685d4f4b1bc7c01 + languageName: node + linkType: hard + "@types/lodash.chunk@npm:^4.2.7": version: 4.2.7 resolution: "@types/lodash.chunk@npm:4.2.7"