Skip to content

Commit

Permalink
feat: Generate public context contract interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
spalladino committed Aug 29, 2023
1 parent e04d1df commit ad8801a
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 42 deletions.
4 changes: 2 additions & 2 deletions docs/docs/dev_docs/contracts/compiling.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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 }
}
Expand Down
5 changes: 5 additions & 0 deletions yarn-project/end-to-end/src/e2e_nested_contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,5 +186,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);
});
});
71 changes: 52 additions & 19 deletions yarn-project/noir-compiler/src/contract-interface-gen/noir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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}(
Expand All @@ -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,
}
`;
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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}
`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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
fn constructor(
Expand Down Expand Up @@ -71,8 +71,7 @@ contract Escrow {
assert(note.address == sender);
assert(note.owner == this);

// TODO: Can we dynamically get this selector?
let _callStackItem = PrivateTokenContractInterface::at(token).transfer(&mut context, amount, recipient);
let _callStackItem = PrivateTokenPrivateContextInterface::at(token).transfer(&mut context, amount, recipient);

context.finish()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ mod test_contract_interface;
// 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::PrivateContext;
use dep::aztec::abi::{ PrivateContextInputs, PublicContextInputs };
use dep::aztec::context::{ PrivateContext, PublicContext };

use crate::test_contract_interface::{
TestContractInterface,
TestPrivateContextInterface,
TestPublicContextInterface,
AStructTestCodeGenStruct,
ADeepStructTestCodeGenStruct,
ANoteADeepStructTestCodeGenStruct,
Expand All @@ -30,7 +31,7 @@ contract ImportTest {
target: Field
) -> distinct pub abi::PrivateCircuitPublicInputs {
let mut context = PrivateContext::new(inputs, abi::hash_args([target]));
let test_contract_instance = TestContractInterface::at(target);
let test_contract_instance = TestPrivateContextInterface::at(target);
let return_values = test_contract_instance.testCodeGen(
&mut context,
1,
Expand Down Expand Up @@ -62,7 +63,7 @@ contract ImportTest {
target: Field
) -> distinct pub abi::PrivateCircuitPublicInputs {
let mut context = PrivateContext::new(inputs, abi::hash_args([target]));
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]);
context.finish()
Expand All @@ -76,9 +77,23 @@ contract ImportTest {
target: Field,
) -> distinct pub abi::PrivateCircuitPublicInputs {
let mut context = PrivateContext::new(inputs, abi::hash_args([target]));
let test_contract_instance = TestContractInterface::at(target);
let test_contract_instance = TestPrivateContextInterface::at(target);
test_contract_instance.createNullifierPublic(&mut context, 1, 2);
context.finish()
}

// 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()
}
}

Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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,
}
}

}


Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,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`.
Expand Down Expand Up @@ -97,7 +97,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);
}

Expand Down Expand Up @@ -131,7 +131,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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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,
}
}

}


Loading

0 comments on commit ad8801a

Please sign in to comment.