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 62860247a85..c199faa8cb5 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]; 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..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,18 +1,23 @@ import { AztecAddress, + BatchCall, 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 } 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 +200,63 @@ 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 contract = await registerContract(testWallet, TestContract); + const receipt = await contract.methods.emit_nullifier(10).send().wait({ debug: true }); + 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 an undeployed contract contract %s', + async kind => { + const testWallet = kind === 'as entrypoint' ? new SignerlessWallet(pxe) : wallet; + const owner = await registerRandomAccount(pxe); + const initArgs: StatefulContractCtorArgs = [owner, 42]; + const contract = await registerContract(testWallet, StatefulTestContract, initArgs); + await contract.methods + .constructor(...initArgs) + .send() + .wait(); + 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); + }); }); + +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-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-contracts/contracts/stateful_test_contract/src/main.nr b/yarn-project/noir-contracts/contracts/stateful_test_contract/src/main.nr index cfca700e66e..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 @@ -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,8 @@ 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)] 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 1901a82fc44..b8f7a7fe114 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"); } @@ -542,15 +541,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(', ')}`, ); }