From 642948652938175f600ed1212f2f83af504d185d Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 1 Feb 2024 11:02:04 -0300 Subject: [PATCH 1/4] feat: Private calls and initialization to undeployed contracts --- .../aztec.js/src/contract/deploy_method.ts | 3 +- .../src/contract/contract_instance.ts | 10 ++--- .../src/structs/complete_address.ts | 8 +++- .../src/e2e_deploy_contract.test.ts | 44 ++++++++++++++++++- .../src/contract-interface-gen/typescript.ts | 2 +- .../src/private_kernel_inner.nr | 10 ----- .../private_functions_tree.ts | 2 +- 7 files changed, 58 insertions(+), 21 deletions(-) diff --git a/yarn-project/aztec.js/src/contract/deploy_method.ts b/yarn-project/aztec.js/src/contract/deploy_method.ts index 7cd77c1a017..32cbd28c7c6 100644 --- a/yarn-project/aztec.js/src/contract/deploy_method.ts +++ b/yarn-project/aztec.js/src/contract/deploy_method.ts @@ -4,7 +4,6 @@ import { ContractDeploymentData, FunctionData, TxContext, - computeContractAddressFromInstance, computePartialAddress, getContractInstanceFromDeployParams, } from '@aztec/circuits.js'; @@ -77,7 +76,7 @@ export class DeployMethod extends Bas const deployParams = [this.artifact, this.args, contractAddressSalt, this.publicKey, portalContract] as const; const instance = getContractInstanceFromDeployParams(...deployParams); - const address = computeContractAddressFromInstance(instance); + const address = instance.address; const contractDeploymentData = new ContractDeploymentData( this.publicKey, diff --git a/yarn-project/circuits.js/src/contract/contract_instance.ts b/yarn-project/circuits.js/src/contract/contract_instance.ts index 01c11167844..6c57db21e37 100644 --- a/yarn-project/circuits.js/src/contract/contract_instance.ts +++ b/yarn-project/circuits.js/src/contract/contract_instance.ts @@ -1,7 +1,7 @@ import { ContractArtifact } from '@aztec/foundation/abi'; import { ContractInstance, ContractInstanceWithAddress } from '@aztec/types/contracts'; -import { EthAddress, Fr, PublicKey, computeContractClassId, getContractClassFromArtifact } from '../index.js'; +import { EthAddress, Fr, Point, PublicKey, computeContractClassId, getContractClassFromArtifact } from '../index.js'; import { computeContractAddressFromInstance, computeInitializationHash, @@ -20,10 +20,10 @@ import { isConstructor } from './contract_tree/contract_tree.js'; */ export function getContractInstanceFromDeployParams( artifact: ContractArtifact, - args: any[], - contractAddressSalt: Fr, - publicKey: PublicKey, - portalContractAddress: EthAddress, + args: any[] = [], + contractAddressSalt: Fr = Fr.random(), + publicKey: PublicKey = Point.ZERO, + portalContractAddress: EthAddress = EthAddress.ZERO, ): ContractInstanceWithAddress { const constructorArtifact = artifact.functions.find(isConstructor); if (!constructorArtifact) { diff --git a/yarn-project/circuits.js/src/structs/complete_address.ts b/yarn-project/circuits.js/src/structs/complete_address.ts index cf34dfa2426..226a3d5a76f 100644 --- a/yarn-project/circuits.js/src/structs/complete_address.ts +++ b/yarn-project/circuits.js/src/structs/complete_address.ts @@ -1,5 +1,5 @@ import { AztecAddress } from '@aztec/foundation/aztec-address'; -import { Fr, Point } from '@aztec/foundation/fields'; +import { Fr, GrumpkinScalar, Point } from '@aztec/foundation/fields'; import { BufferReader } from '@aztec/foundation/serialize'; import { Grumpkin } from '../barretenberg/index.js'; @@ -48,6 +48,12 @@ export class CompleteAddress { return new CompleteAddress(address, publicKey, partialAddress); } + static fromRandomPrivateKey() { + const privateKey = GrumpkinScalar.random(); + const partialAddress = Fr.random(); + return { privateKey, completeAddress: CompleteAddress.fromPrivateKeyAndPartialAddress(privateKey, partialAddress) }; + } + static fromPrivateKeyAndPartialAddress(privateKey: GrumpkinPrivateKey, partialAddress: Fr): CompleteAddress { const grumpkin = new Grumpkin(); const publicKey = grumpkin.mul(Grumpkin.generator, privateKey); diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts index f5ffb8b415d..1fb745ccf2d 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts @@ -2,17 +2,21 @@ import { AztecAddress, CompleteAddress, Contract, + ContractArtifact, ContractDeployer, DebugLogger, EthAddress, Fr, PXE, + SignerlessWallet, TxStatus, Wallet, getContractInstanceFromDeployParams, isContractDeployed, } from '@aztec/aztec.js'; -import { TestContractArtifact } from '@aztec/noir-contracts/Test'; +import { siloNullifier } from '@aztec/circuits.js/abis'; +import { StatefulTestContract, StatefulTestContractArtifact } from '@aztec/noir-contracts'; +import { TestContract, TestContractArtifact } from '@aztec/noir-contracts/Test'; import { TokenContractArtifact } from '@aztec/noir-contracts/Token'; import { SequencerClient } from '@aztec/sequencer-client'; @@ -195,4 +199,42 @@ describe('e2e_deploy_contract', () => { }); } }, 60_000); + + // Tests calling a private function in an uninitialized and undeployed contract. Note that + // it still requires registering the contract artifact and instance locally in the pxe. + test.each(['as entrypoint', 'from an account contract'] as const)( + 'executes a function in an undeployed contract %s', + async kind => { + const testWallet = kind === 'as entrypoint' ? new SignerlessWallet(pxe) : wallet; + const instance = await registerContract(pxe, TestContractArtifact); + const contract = await TestContract.at(instance.address, testWallet); + const receipt = await contract.methods.emit_nullifier(10).send().wait({ debug: true }); + const expected = siloNullifier(instance.address, new Fr(10)); + expect(receipt.debugInfo?.newNullifiers[1]).toEqual(expected); + }, + ); + + // Tests privately initializing an undeployed contract. Also requires pxe registration in advance. + test.each(['as entrypoint', 'from an account contract'] as const)( + 'privately initializes a contract %s', + async kind => { + const testWallet = kind === 'as entrypoint' ? new SignerlessWallet(pxe) : wallet; + const { completeAddress: owner, privateKey } = CompleteAddress.fromRandomPrivateKey(); + await pxe.registerAccount(privateKey, owner.partialAddress); + const initArgs: Parameters = [owner, 42]; + const instance = await registerContract(pxe, StatefulTestContractArtifact, initArgs); + const contract = await StatefulTestContract.at(instance.address, testWallet); + await contract.methods + .constructor(...initArgs) + .send() + .wait(); + expect(await contract.methods.summed_values(owner).view()).toEqual(42n); + }, + ); }); + +async function registerContract(pxe: PXE, artifact: ContractArtifact, args: any[] = []) { + const instance = getContractInstanceFromDeployParams(artifact, args); + await pxe.addContracts([{ artifact, instance }]); + return instance; +} diff --git a/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts b/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts index 2054e9ddc7e..73c0c253623 100644 --- a/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts +++ b/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts @@ -169,7 +169,7 @@ function generateAbiStatement(name: string, artifactImportPath: string) { * @returns The corresponding ts code. */ export function generateTypescriptContractInterface(input: ContractArtifact, artifactImportPath?: string) { - const methods = input.functions.filter(f => f.name !== 'constructor' && !f.isInternal).map(generateMethod); + const methods = input.functions.filter(f => !f.isInternal).map(generateMethod); const deploy = artifactImportPath && generateDeploy(input); const ctor = artifactImportPath && generateConstructor(input.name); const at = artifactImportPath && generateAt(input.name); diff --git a/yarn-project/noir-protocol-circuits/src/crates/private-kernel-lib/src/private_kernel_inner.nr b/yarn-project/noir-protocol-circuits/src/crates/private-kernel-lib/src/private_kernel_inner.nr index 47e378306c3..84d7f0b9c27 100644 --- a/yarn-project/noir-protocol-circuits/src/crates/private-kernel-lib/src/private_kernel_inner.nr +++ b/yarn-project/noir-protocol-circuits/src/crates/private-kernel-lib/src/private_kernel_inner.nr @@ -32,7 +32,6 @@ impl PrivateKernelInputsInner { let this_call_stack_item = self.private_call.call_stack_item; let function_data = this_call_stack_item.function_data; assert(function_data.is_private, "Private kernel circuit can only execute a private function"); - assert(function_data.is_constructor == false, "A constructor must be executed as the first tx in the recursion"); assert(self.previous_kernel.public_inputs.is_private, "Can only verify a private kernel snark in the private kernel circuit"); } @@ -543,15 +542,6 @@ mod tests { builder.failed(); } - #[test(should_fail_with="A constructor must be executed as the first tx in the recursion")] - fn private_function_is_constructor_fails() { - let mut builder = PrivateKernelInnerInputsBuilder::new(); - - builder.private_call.function_data.is_constructor = true; - - builder.failed(); - } - #[test(should_fail_with="Can only verify a private kernel snark in the private kernel circuit")] fn previous_kernel_is_private_false_fails() { let mut builder = PrivateKernelInnerInputsBuilder::new(); diff --git a/yarn-project/pxe/src/contract_data_oracle/private_functions_tree.ts b/yarn-project/pxe/src/contract_data_oracle/private_functions_tree.ts index aabda34ccf4..5ae3ea6b0d1 100644 --- a/yarn-project/pxe/src/contract_data_oracle/private_functions_tree.ts +++ b/yarn-project/pxe/src/contract_data_oracle/private_functions_tree.ts @@ -43,7 +43,7 @@ export class PrivateFunctionsTree { if (!artifact) { throw new Error( `Unknown function. Selector ${selector.toString()} not found in the artifact of contract ${this.contract.instance.address.toString()}. Expected one of: ${this.contract.functions - .map(f => f.selector.toString()) + .map(f => `${f.name} (${f.selector.toString()})`) .join(', ')}`, ); } From e7f969381ab675c573dc2f2c785e651893768dff Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 1 Feb 2024 11:39:00 -0300 Subject: [PATCH 2/4] Moar tests! --- .../src/e2e_deploy_contract.test.ts | 50 +++++++++++++------ .../stateful_test_contract/src/main.nr | 13 +++-- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts index 1fb745ccf2d..83023b8505a 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts @@ -1,5 +1,6 @@ import { AztecAddress, + BatchCall, CompleteAddress, Contract, ContractArtifact, @@ -15,7 +16,7 @@ import { isContractDeployed, } from '@aztec/aztec.js'; import { siloNullifier } from '@aztec/circuits.js/abis'; -import { StatefulTestContract, StatefulTestContractArtifact } from '@aztec/noir-contracts'; +import { StatefulTestContract } from '@aztec/noir-contracts'; import { TestContract, TestContractArtifact } from '@aztec/noir-contracts/Test'; import { TokenContractArtifact } from '@aztec/noir-contracts/Token'; import { SequencerClient } from '@aztec/sequencer-client'; @@ -206,24 +207,21 @@ describe('e2e_deploy_contract', () => { 'executes a function in an undeployed contract %s', async kind => { const testWallet = kind === 'as entrypoint' ? new SignerlessWallet(pxe) : wallet; - const instance = await registerContract(pxe, TestContractArtifact); - const contract = await TestContract.at(instance.address, testWallet); + const contract = await registerContract(testWallet, TestContract); const receipt = await contract.methods.emit_nullifier(10).send().wait({ debug: true }); - const expected = siloNullifier(instance.address, new Fr(10)); + const expected = siloNullifier(contract.address, new Fr(10)); expect(receipt.debugInfo?.newNullifiers[1]).toEqual(expected); }, ); // Tests privately initializing an undeployed contract. Also requires pxe registration in advance. test.each(['as entrypoint', 'from an account contract'] as const)( - 'privately initializes a contract %s', + 'privately initializes an undeployed contract contract %s', async kind => { const testWallet = kind === 'as entrypoint' ? new SignerlessWallet(pxe) : wallet; - const { completeAddress: owner, privateKey } = CompleteAddress.fromRandomPrivateKey(); - await pxe.registerAccount(privateKey, owner.partialAddress); - const initArgs: Parameters = [owner, 42]; - const instance = await registerContract(pxe, StatefulTestContractArtifact, initArgs); - const contract = await StatefulTestContract.at(instance.address, testWallet); + const owner = await registerRandomAccount(pxe); + const initArgs: StatefulContractCtorArgs = [owner, 42]; + const contract = await registerContract(testWallet, StatefulTestContract, initArgs); await contract.methods .constructor(...initArgs) .send() @@ -231,10 +229,34 @@ describe('e2e_deploy_contract', () => { expect(await contract.methods.summed_values(owner).view()).toEqual(42n); }, ); + + // Tests privately initializing multiple undeployed contracts on the same tx through an account contract. + it('initializes multiple undeployed contracts in a single tx', async () => { + const owner = await registerRandomAccount(pxe); + const initArgs: StatefulContractCtorArgs[] = [42, 52].map(value => [owner, value]); + const contracts = await Promise.all(initArgs.map(args => registerContract(wallet, StatefulTestContract, args))); + const calls = contracts.map((c, i) => c.methods.constructor(...initArgs[i]).request()); + await new BatchCall(wallet, calls).send().wait(); + expect(await contracts[0].methods.summed_values(owner).view()).toEqual(42n); + expect(await contracts[1].methods.summed_values(owner).view()).toEqual(52n); + }); }); -async function registerContract(pxe: PXE, artifact: ContractArtifact, args: any[] = []) { - const instance = getContractInstanceFromDeployParams(artifact, args); - await pxe.addContracts([{ artifact, instance }]); - return instance; +type StatefulContractCtorArgs = Parameters; + +async function registerRandomAccount(pxe: PXE): Promise { + const { completeAddress: owner, privateKey } = CompleteAddress.fromRandomPrivateKey(); + await pxe.registerAccount(privateKey, owner.partialAddress); + return owner.address; +} + +type ContractArtifactClass = { + at(address: AztecAddress, wallet: Wallet): Promise; + artifact: ContractArtifact; +}; + +async function registerContract(wallet: Wallet, contractArtifact: ContractArtifactClass, args: any[] = []) { + const instance = getContractInstanceFromDeployParams(contractArtifact.artifact, args); + await wallet.addContracts([{ artifact: contractArtifact.artifact, instance }]); + return contractArtifact.at(instance.address, wallet); } diff --git a/yarn-project/noir-contracts/contracts/stateful_test_contract/src/main.nr b/yarn-project/noir-contracts/contracts/stateful_test_contract/src/main.nr index cfca700e66e..f9e846c91ea 100644 --- a/yarn-project/noir-contracts/contracts/stateful_test_contract/src/main.nr +++ b/yarn-project/noir-contracts/contracts/stateful_test_contract/src/main.nr @@ -1,6 +1,9 @@ // A contract used for testing a random hodgepodge of small features from simulator and end-to-end tests. contract StatefulTest { - use dep::aztec::protocol_types::address::AztecAddress; + use dep::aztec::protocol_types::{ + address::AztecAddress, + abis::function_selector::FunctionSelector, + }; use dep::std::option::Option; use dep::value_note::{ balance_utils, @@ -47,8 +50,12 @@ contract StatefulTest { #[aztec(private)] fn constructor(owner: AztecAddress, value: Field) { - let loc = storage.notes.at(owner); - increment(loc, value, owner); + let selector = FunctionSelector::from_signature("create_note((Field),Field)"); + let _res = context.call_private_function( + context.this_address(), + selector, + [owner.to_field(), value] + ); } #[aztec(private)] From 7e765b57c754ea5c7707df19a8c837dfd685ce82 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 1 Feb 2024 11:48:21 -0300 Subject: [PATCH 3/4] Format --- .../contracts/stateful_test_contract/src/main.nr | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/yarn-project/noir-contracts/contracts/stateful_test_contract/src/main.nr b/yarn-project/noir-contracts/contracts/stateful_test_contract/src/main.nr index f9e846c91ea..b1e22e2eb42 100644 --- a/yarn-project/noir-contracts/contracts/stateful_test_contract/src/main.nr +++ b/yarn-project/noir-contracts/contracts/stateful_test_contract/src/main.nr @@ -51,11 +51,7 @@ contract StatefulTest { #[aztec(private)] fn constructor(owner: AztecAddress, value: Field) { let selector = FunctionSelector::from_signature("create_note((Field),Field)"); - let _res = context.call_private_function( - context.this_address(), - selector, - [owner.to_field(), value] - ); + let _res = context.call_private_function(context.this_address(), selector, [owner.to_field(), value]); } #[aztec(private)] From f0ab19311bba70eb8c8c82cf4ddc8be5bd17ca4c Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 1 Feb 2024 13:05:14 -0300 Subject: [PATCH 4/4] Fix test --- .../acir-simulator/src/client/private_execution.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 0587f3ec4dd..c5536243fb5 100644 --- a/yarn-project/acir-simulator/src/client/private_execution.test.ts +++ b/yarn-project/acir-simulator/src/client/private_execution.test.ts @@ -277,11 +277,18 @@ describe('Private Execution test suite', () => { oracle.getFunctionArtifactByName.mockImplementation((_, functionName: string) => Promise.resolve(getFunctionArtifact(StatefulTestContractArtifact, functionName)), ); + + oracle.getFunctionArtifact.mockImplementation((_, selector: FunctionSelector) => + Promise.resolve(getFunctionArtifact(StatefulTestContractArtifact, selector)), + ); + + oracle.getPortalContractAddress.mockResolvedValue(EthAddress.ZERO); }); it('should have a constructor with arguments that inserts notes', async () => { const artifact = getFunctionArtifact(StatefulTestContractArtifact, 'constructor'); - const result = await runSimulator({ args: [owner, 140], artifact }); + const topLevelResult = await runSimulator({ args: [owner, 140], artifact }); + const result = topLevelResult.nestedExecutions[0]; expect(result.newNotes).toHaveLength(1); const newNote = result.newNotes[0];