Skip to content

Commit

Permalink
feat(noir): autogenerate contract interface for calling from external…
Browse files Browse the repository at this point in the history
… 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
noir-lang/noir#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
noir-lang/noir#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 <[email protected]>
  • Loading branch information
spalladino and iAmMichaelConnor authored Aug 14, 2023
1 parent 3ffee7e commit e9d0e6b
Show file tree
Hide file tree
Showing 31 changed files with 970 additions and 267 deletions.
47 changes: 46 additions & 1 deletion yarn-project/acir-simulator/src/client/private_execution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
computeContractAddressFromPartial,
computeSecretMessageHash,
computeUniqueCommitment,
computeVarArgsHash,
siloCommitment,
} from '@aztec/circuits.js/abis';
import { pedersenPlookupCommitInputs } from '@aztec/circuits.js/barretenberg';
Expand All @@ -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,
Expand Down Expand Up @@ -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');

Expand Down
293 changes: 162 additions & 131 deletions yarn-project/end-to-end/src/e2e_nested_contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 () => {
Expand All @@ -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);
});
});
2 changes: 1 addition & 1 deletion yarn-project/foundation/src/abi/abi_coder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
Loading

0 comments on commit e9d0e6b

Please sign in to comment.