From 2f4045e22dbea0e316103da20c6ba8a667826777 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 30 Aug 2023 09:59:31 -0300 Subject: [PATCH] feat: Generate public context contract interfaces (#1860) Generates two interface structs for each contract `Foo`: a `FooPrivateContextInterface` and a `FooPublicContextInterface`. The former is exactly as the one we had already, while the latter only contains public methods and uses public contexts instead of private ones. Fixes #1782 --- docs/docs/dev_docs/contracts/compiling.md | 4 +- .../src/e2e_nested_contract.test.ts | 5 ++ .../src/contract-interface-gen/noir.ts | 71 ++++++++++++++----- .../src/contracts/escrow_contract/src/main.nr | 4 +- .../import_test_contract/src/main.nr | 27 +++++-- .../src/interface.nr | 26 ++++++- .../src/main.nr | 6 +- .../private_token_contract/src/interface.nr | 26 ++++++- .../contracts/test_contract/src/interface.nr | 65 ++++++++++++++++- 9 files changed, 193 insertions(+), 41 deletions(-) diff --git a/docs/docs/dev_docs/contracts/compiling.md b/docs/docs/dev_docs/contracts/compiling.md index 2e44cc917b7..52a0c672fd3 100644 --- a/docs/docs/dev_docs/contracts/compiling.md +++ b/docs/docs/dev_docs/contracts/compiling.md @@ -74,7 +74,7 @@ Read more about interacting with contracts using `aztec.js` [here](../dapps/main A Noir contract can [call a function](./functions.md) in another contract via `context.call_private_function` or `context.call_public_function`. However, this requires manually assembling the function selector and manually serialising the arguments, which is not type-safe. -To make this easier, the compiler can generate a contract interface struct that exposes a convenience method for each function listed in a given contract ABI. These structs are intended to be used from another contract project that calls into the current one. +To make this easier, the compiler can generate contract interface structs that expose a convenience method for each function listed in a given contract ABI. These structs are intended to be used from another contract project that calls into the current one. For each contract, two interface structs are generated: one to be used from private functions with a `PrivateContext`, and one to be used from open functions with a `PublicContext`. To generate them, include a `--interface` option in the compile command with a path to the target folder for the generated Noir interface files: @@ -85,7 +85,7 @@ aztec-cli compile --interface ./path/to/another_aztec_contract_project/src ./pat Example code generated from the [PrivateToken](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/noir-contracts/src/contracts/private_token_contract/src/main.nr) contract: ```rust -impl PrivateTokenContractInterface { +impl PrivateTokenPrivateContextInterface { fn at(address: Field) -> Self { Self { address } } 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 370f931f006..ae58b45e67e 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 @@ -137,5 +137,10 @@ describe('e2e_nested_contract', () => { logger(`Calling openfn on importer contract`); await importerContract.methods.callOpenFn(testContract.address).send().wait(); }, 30_000); + + it('calls an open function from an open function', async () => { + logger(`Calling pub openfn on importer contract`); + await importerContract.methods.pubCallOpenFn(testContract.address).send().wait(); + }, 30_000); }); }); diff --git a/yarn-project/noir-compiler/src/contract-interface-gen/noir.ts b/yarn-project/noir-compiler/src/contract-interface-gen/noir.ts index 8de53482834..cbd9cea1532 100644 --- a/yarn-project/noir-compiler/src/contract-interface-gen/noir.ts +++ b/yarn-project/noir-compiler/src/contract-interface-gen/noir.ts @@ -9,6 +9,7 @@ import { } from '@aztec/foundation/abi'; import camelCase from 'lodash.camelcase'; +import capitalize from 'lodash.capitalize'; import compact from 'lodash.compact'; import times from 'lodash.times'; import upperFirst from 'lodash.upperfirst'; @@ -134,20 +135,20 @@ function generateSerialisation(parameters: ABIParameter[]) { /** * 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. + * @param functionData - Data relating to the function, which can be used to generate a callable Noir Function. + * @param kind - Whether this interface will be used from private or public functions. + * @returns A code string. */ -function generateFunctionInterface(functionData: FunctionAbi) { +function generateFunctionInterface(functionData: FunctionAbi, kind: 'private' | 'public') { const { name, parameters } = functionData; const selector = FunctionSelector.fromNameAndParameters(name, parameters); const serialisation = generateSerialisation(parameters); + const contextType = kind === 'private' ? '&mut PrivateContext' : 'PublicContext'; const callStatement = generateCallStatement(selector, functionData.functionType); - const allParams = [ - 'self', - 'context: &mut PrivateContext', - ...parameters.map(p => generateParameter(p, functionData)), - ]; - const retType = isPrivateCall(functionData.functionType) ? `-> [Field; RETURN_VALUES_LENGTH] ` : ``; + const allParams = ['self', `context: ${contextType}`, ...parameters.map(p => generateParameter(p, functionData))]; + const isPrivate = isPrivateCall(functionData.functionType); + const isSync = (isPrivate && kind === 'private') || (!isPrivate && kind === 'public'); + const retType = isSync ? `-> [Field; RETURN_VALUES_LENGTH] ` : ``; return ` fn ${name}( @@ -165,17 +166,29 @@ ${callStatement} */ function generateStaticImports() { return `use dep::std; -use dep::aztec::context::PrivateContext; +use dep::aztec::context::{ PrivateContext, PublicContext }; use dep::aztec::constants_gen::RETURN_VALUES_LENGTH;`; } +/** + * Generates the name of the contract struct, based on whether it's for private or public usage. + * @param contractName - Name of the contract. + * @param kind - Whether this interface will be used from private or public functions. + * @returns A name. + */ +function generateContractStructName(contractName: string, kind: 'private' | 'public') { + return `${contractName}${capitalize(kind)}ContextInterface`; +} + /** * 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. + * @param kind - Whether this interface will be used from private or public functions. * @returns Code. */ -function generateContractInterfaceStruct(contractName: string) { - return `struct ${contractName}ContractInterface { +function generateContractInterfaceStruct(contractName: string, kind: 'private' | 'public') { + return `// Interface for calling ${contractName} functions from a ${kind} context +struct ${generateContractStructName(contractName, kind)} { address: Field, } `; @@ -184,11 +197,12 @@ function generateContractInterfaceStruct(contractName: string) { /** * Generates the implementation of the contract interface struct. * @param contractName - The name of the contract, as matches the original source file. + * @param kind - Whether this interface will be used from private or public functions. * @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 { +function generateContractInterfaceImpl(contractName: string, kind: 'private' | 'public', functions: string[]) { + return `impl ${generateContractStructName(contractName, kind)} { fn at(address: Field) -> Self { Self { address, @@ -237,6 +251,25 @@ function collectStructs(params: ABIVariable[], parentNames: string[]): StructInf return structs; } +/** + * Generates the struct definition and implementation for a contract interface. + * @param abiName - Name of the contract. + * @param kind - Whether this interface will be used from private or public functions. + * @param methods - Contract methods to generate (private ones will be excluded if kind is public) + * @returns Code. + */ +function generateContractStruct(abiName: string, kind: 'private' | 'public', methods: FunctionAbi[]) { + const contractStruct: string = generateContractInterfaceStruct(abiName, kind); + const applicableMethods = methods.filter(m => kind === 'private' || !isPrivateCall(m.functionType)); + const functionInterfaces = applicableMethods.map(m => generateFunctionInterface(m, kind)); + const contractImpl: string = generateContractInterfaceImpl(abiName, kind, functionInterfaces); + + return ` +${contractStruct} +${contractImpl} + `; +} + /** * Generates the Noir code to represent an interface for calling a contract. * @param abi - The compiled Noir artifact. @@ -249,17 +282,17 @@ export function generateNoirContractInterface(abi: ContractAbi) { 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); + const privateContractStruct = generateContractStruct(abi.name, 'private', methods); + const publicContractStruct = generateContractStruct(abi.name, 'public', methods); return `/* Autogenerated file, do not edit! */ ${generateStaticImports()} ${paramStructs.join('\n')} -${contractStruct} -${contractImpl} +${privateContractStruct} + +${publicContractStruct} `; } diff --git a/yarn-project/noir-contracts/src/contracts/escrow_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/escrow_contract/src/main.nr index 598ceb493ba..af56d603135 100644 --- a/yarn-project/noir-contracts/src/contracts/escrow_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/escrow_contract/src/main.nr @@ -28,7 +28,7 @@ contract Escrow { use crate::storage::Storage; - use crate::private_token_contract_interface::PrivateTokenContractInterface; + use crate::private_token_contract_interface::PrivateTokenPrivateContextInterface; // Creates a new instance #[aztec(private)] @@ -68,7 +68,7 @@ contract Escrow { assert(note.address == sender); assert(note.owner == this); - let _callStackItem = PrivateTokenContractInterface::at(token).transfer(&mut context, amount, recipient); + let _callStackItem = PrivateTokenPrivateContextInterface::at(token).transfer(&mut context, amount, recipient); } unconstrained fn compute_note_hash_and_nullifier(contract_address: Field, nonce: Field, storage_slot: Field, preimage: [Field; ADDRESS_NOTE_LEN]) -> [Field; 4] { 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 index eec557b7487..7eaf4e53afb 100644 --- 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 @@ -5,11 +5,12 @@ mod test_contract_interface; contract ImportTest { use dep::aztec::abi; use dep::aztec::abi::Hasher; - use dep::aztec::abi::PrivateContextInputs; - use dep::aztec::context::PrivateContext; + use dep::aztec::abi::{ PrivateContextInputs, PublicContextInputs }; + use dep::aztec::context::{ PrivateContext, PublicContext }; use crate::test_contract_interface::{ - TestContractInterface, + TestPrivateContextInterface, + TestPublicContextInterface, AStructTestCodeGenStruct, ADeepStructTestCodeGenStruct, ANoteADeepStructTestCodeGenStruct, @@ -29,7 +30,7 @@ contract ImportTest { fn main( target: Field ) { - let test_contract_instance = TestContractInterface::at(target); + let test_contract_instance = TestPrivateContextInterface::at(target); let return_values = test_contract_instance.testCodeGen( &mut context, 1, @@ -59,7 +60,7 @@ contract ImportTest { fn callNoArgs( target: Field ) { - let test_contract_instance = TestContractInterface::at(target); + let test_contract_instance = TestPrivateContextInterface::at(target); let return_values = test_contract_instance.getThisAddress(&mut context); context.return_values.push(return_values[0]); } @@ -71,8 +72,22 @@ contract ImportTest { fn callOpenFn( target: Field, ) { - let test_contract_instance = TestContractInterface::at(target); + let test_contract_instance = TestPrivateContextInterface::at(target); test_contract_instance.createNullifierPublic(&mut context, 1, 2); } + + // Calls the createNullifierPublic on the Test contract at the target address + // Used for testing calling an open function from another open function + // See yarn-project/end-to-end/src/e2e_nested_contract.test.ts + open fn pubCallOpenFn( + inputs: PublicContextInputs, + target: Field, + ) -> pub abi::PublicCircuitPublicInputs { + let mut context = PublicContext::new(inputs, abi::hash_args([target])); + let test_contract_instance = TestPublicContextInterface::at(target); + let ret = test_contract_instance.createNullifierPublic(context, 1, 2); + context.return_values.push(ret[0]); + context.finish() + } } diff --git a/yarn-project/noir-contracts/src/contracts/private_token_airdrop_contract/src/interface.nr b/yarn-project/noir-contracts/src/contracts/private_token_airdrop_contract/src/interface.nr index 52b59aa68fa..975acddead8 100644 --- a/yarn-project/noir-contracts/src/contracts/private_token_airdrop_contract/src/interface.nr +++ b/yarn-project/noir-contracts/src/contracts/private_token_airdrop_contract/src/interface.nr @@ -1,15 +1,17 @@ /* Autogenerated file, do not edit! */ use dep::std; -use dep::aztec::context::PrivateContext; +use dep::aztec::context::{ PrivateContext, PublicContext }; use dep::aztec::constants_gen::RETURN_VALUES_LENGTH; -struct PrivateTokenAirdropContractInterface { + +// Interface for calling PrivateTokenAirdrop functions from a private context +struct PrivateTokenAirdropPrivateContextInterface { address: Field, } -impl PrivateTokenAirdropContractInterface { +impl PrivateTokenAirdropPrivateContextInterface { fn at(address: Field) -> Self { Self { address, @@ -112,4 +114,22 @@ impl PrivateTokenAirdropContractInterface { } } + + + + +// Interface for calling PrivateTokenAirdrop functions from a public context +struct PrivateTokenAirdropPublicContextInterface { + address: Field, +} +impl PrivateTokenAirdropPublicContextInterface { + fn at(address: Field) -> Self { + Self { + address, + } + } + +} + + diff --git a/yarn-project/noir-contracts/src/contracts/private_token_airdrop_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/private_token_airdrop_contract/src/main.nr index 7bde51e9683..f5db41b8dad 100644 --- a/yarn-project/noir-contracts/src/contracts/private_token_airdrop_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/private_token_airdrop_contract/src/main.nr @@ -23,7 +23,7 @@ contract PrivateTokenAirdrop { use crate::storage::Storage; use crate::claim_note::{ClaimNote, ClaimNoteMethods}; - use crate::interface::PrivateTokenAirdropContractInterface; + use crate::interface::PrivateTokenAirdropPrivateContextInterface; // Constructs the contract and sets `initial_supply` which is fully owned by `owner`. @@ -79,7 +79,7 @@ contract PrivateTokenAirdrop { if sum != amount { // The destroyed notes' sum is not enough. Keep burning. let amount_to_burn = amount - sum; - let this = PrivateTokenAirdropContractInterface::at(this_address); + let this = PrivateTokenAirdropPrivateContextInterface::at(this_address); let _res = this.burn(&mut context, amount_to_burn, owner); } } @@ -106,7 +106,7 @@ contract PrivateTokenAirdrop { // We only call burn() when decrement_by_at_most() didn't destroy enough notes. let amount_to_burn = amount - sum; let this_address = context.this_address(); - let this = PrivateTokenAirdropContractInterface::at(this_address); + let this = PrivateTokenAirdropPrivateContextInterface::at(this_address); let _res = this.burn(&mut context, amount_to_burn, sender); } diff --git a/yarn-project/noir-contracts/src/contracts/private_token_contract/src/interface.nr b/yarn-project/noir-contracts/src/contracts/private_token_contract/src/interface.nr index f35be0af102..c308d2e8886 100644 --- a/yarn-project/noir-contracts/src/contracts/private_token_contract/src/interface.nr +++ b/yarn-project/noir-contracts/src/contracts/private_token_contract/src/interface.nr @@ -1,15 +1,17 @@ /* Autogenerated file, do not edit! */ use dep::std; -use dep::aztec::context::PrivateContext; +use dep::aztec::context::{ PrivateContext, PublicContext }; use dep::aztec::constants_gen::RETURN_VALUES_LENGTH; -struct PrivateTokenContractInterface { + +// Interface for calling PrivateToken functions from a private context +struct PrivateTokenPrivateContextInterface { address: Field, } -impl PrivateTokenContractInterface { +impl PrivateTokenPrivateContextInterface { fn at(address: Field) -> Self { Self { address, @@ -44,4 +46,22 @@ impl PrivateTokenContractInterface { } } + + + + +// Interface for calling PrivateToken functions from a public context +struct PrivateTokenPublicContextInterface { + address: Field, +} +impl PrivateTokenPublicContextInterface { + fn at(address: Field) -> Self { + Self { + address, + } + } + +} + + diff --git a/yarn-project/noir-contracts/src/contracts/test_contract/src/interface.nr b/yarn-project/noir-contracts/src/contracts/test_contract/src/interface.nr index c8476ff3558..7ccee2964e7 100644 --- a/yarn-project/noir-contracts/src/contracts/test_contract/src/interface.nr +++ b/yarn-project/noir-contracts/src/contracts/test_contract/src/interface.nr @@ -1,7 +1,7 @@ /* Autogenerated file, do not edit! */ use dep::std; -use dep::aztec::context::PrivateContext; +use dep::aztec::context::{ PrivateContext, PublicContext }; use dep::aztec::constants_gen::RETURN_VALUES_LENGTH; struct AStructTestCodeGenStruct { @@ -26,11 +26,13 @@ struct ManyNotesADeepStructTestCodeGenStruct { secretHash: Field, } -struct TestContractInterface { + +// Interface for calling Test functions from a private context +struct TestPrivateContextInterface { address: Field, } -impl TestContractInterface { +impl TestPrivateContextInterface { fn at(address: Field) -> Self { Self { address, @@ -154,4 +156,61 @@ impl TestContractInterface { } } + + + + +// Interface for calling Test functions from a public context +struct TestPublicContextInterface { + address: Field, +} + +impl TestPublicContextInterface { + fn at(address: Field) -> Self { + Self { + address, + } + } + + fn createL2ToL1MessagePublic( + self, + context: PublicContext, + amount: Field, + secretHash: Field + ) -> [Field; RETURN_VALUES_LENGTH] { + 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: PublicContext, + amount: Field, + secretHash: Field + ) -> [Field; RETURN_VALUES_LENGTH] { + let mut serialised_args = [0; 2]; + serialised_args[0] = amount; + serialised_args[1] = secretHash; + + context.call_public_function(self.address, 0x0217ef40, serialised_args) + } + + + fn isTimeEqual( + self, + context: PublicContext, + time: Field + ) -> [Field; RETURN_VALUES_LENGTH] { + let mut serialised_args = [0; 1]; + serialised_args[0] = time; + + context.call_public_function(self.address, 0x57587e4d, serialised_args) + } + +} + +