From e9d0e6bbe6645c6f9a303f99c9952fc2ce7bcb03 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Mon, 14 Aug 2023 08:13:54 -0300 Subject: [PATCH] feat(noir): autogenerate contract interface for calling from external contracts (#1487) Builds on @iAmMichaelConnor work to generate a contract interface for simplifying calling function in other contracts. Uses only information present in the ABI. For each function in the target contract, creates a wrapper function that receives the same arguments, serialises them based on the standard ABI encoding format (see [here](https://github.com/AztecProtocol/aztec-packages/blob/49d272159f1b27521ad34081c7f1622ccac19dff/yarn-project/foundation/src/abi/encoder.ts)), and uses the `call_private_function` from the `context` to call into them. To handle custom structs, we're re-defining them in the contract interface. Until we get struct type names in the ABI (see https://github.com/noir-lang/noir/issues/2238), we are autogenerating the name as well, based on the function name and param name where they are used. Serialisation is done manually in the code, but we may want to replace it with a Noir intrinsic when available (see https://github.com/noir-lang/noir/issues/2240). See [this file](https://github.com/AztecProtocol/aztec-packages/blob/49d272159f1b27521ad34081c7f1622ccac19dff/yarn-project/noir-contracts/src/contracts/test_contract/src/test_contract_interface.nr) for example output. Fixes #1237 --------- Co-authored-by: iAmMichaelConnor --- .../src/client/private_execution.test.ts | 47 ++- .../src/e2e_nested_contract.test.ts | 293 ++++++++++-------- yarn-project/foundation/src/abi/abi_coder.ts | 2 +- yarn-project/noir-compiler/package.json | 8 + yarn-project/noir-compiler/src/cli.ts | 4 +- .../src/contract-interface-gen/noir.ts | 261 ++++++++++++++++ .../typescript.ts} | 57 ++-- yarn-project/noir-compiler/src/index.ts | 3 +- yarn-project/noir-contracts/scripts/types.sh | 5 +- .../contracts/child_contract/src/storage.nr | 4 +- .../contracts/import_test_contract/Nargo.toml | 8 + .../import_test_contract/src/main.nr | 84 +++++ .../src/test_contract_interface.nr | 1 + .../contracts/lending_contract/src/storage.nr | 6 +- .../non_native_token_contract/src/storage.nr | 16 +- .../src/contracts/parent_contract/src/main.nr | 12 +- .../public_token_contract/src/storage.nr | 9 +- .../src/contracts/test_contract/src/main.nr | 52 +++- .../src/test_contract_interface.nr | 145 +++++++++ .../noir-contracts/src/scripts/copy_output.ts | 116 +++---- yarn-project/noir-libs/noir-aztec/src/abi.nr | 30 +- .../noir-libs/noir-aztec/src/context.nr | 8 +- .../noir-libs/noir-aztec/src/state_vars.nr | 1 - .../noir-aztec/src/state_vars/public_state.nr | 2 +- .../noir-libs/noir-aztec/src/types.nr | 3 +- .../noir-libs/noir-aztec/src/types/point.nr | 22 +- .../type_serialisation.nr | 1 + .../type_serialisation/bool_serialisation.nr | 16 + .../type_serialisation/field_serialisation.nr | 2 +- .../type_serialisation/u32_serialisation.nr | 2 +- yarn-project/yarn.lock | 17 + 31 files changed, 970 insertions(+), 267 deletions(-) create mode 100644 yarn-project/noir-compiler/src/contract-interface-gen/noir.ts rename yarn-project/noir-compiler/src/{typegen/index.ts => contract-interface-gen/typescript.ts} (97%) create mode 100644 yarn-project/noir-contracts/src/contracts/import_test_contract/Nargo.toml create mode 100644 yarn-project/noir-contracts/src/contracts/import_test_contract/src/main.nr create mode 120000 yarn-project/noir-contracts/src/contracts/import_test_contract/src/test_contract_interface.nr create mode 100644 yarn-project/noir-contracts/src/contracts/test_contract/src/test_contract_interface.nr rename yarn-project/noir-libs/noir-aztec/src/{state_vars => types}/type_serialisation.nr (92%) create mode 100644 yarn-project/noir-libs/noir-aztec/src/types/type_serialisation/bool_serialisation.nr rename yarn-project/noir-libs/noir-aztec/src/{state_vars => types}/type_serialisation/field_serialisation.nr (82%) rename yarn-project/noir-libs/noir-aztec/src/{state_vars => types}/type_serialisation/u32_serialisation.nr (82%) 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"