diff --git a/noir-projects/aztec-nr/aztec/src/initializer.nr b/noir-projects/aztec-nr/aztec/src/initializer.nr index bb39faacca9d..b33c49274cf1 100644 --- a/noir-projects/aztec-nr/aztec/src/initializer.nr +++ b/noir-projects/aztec-nr/aztec/src/initializer.nr @@ -1,5 +1,5 @@ use dep::protocol_types::hash::silo_nullifier; -use crate::context::{PrivateContext, ContextInterface}; +use crate::context::{PrivateContext, PublicContext, ContextInterface}; use crate::history::nullifier_inclusion::prove_nullifier_inclusion; pub fn mark_as_initialized(context: &mut PrivateContext) { @@ -7,6 +7,14 @@ pub fn mark_as_initialized(context: &mut PrivateContext) { context.push_new_nullifier(init_nullifier, 0); } +// TODO(@spalladino): Using the trait here fails with "No matching impl found for `&mut TContext: ContextInterface`" +// on the `push_new_nullifier` call. Remove this method in favor of a single method that uses the trait (and update +// the noir compiler macro accordingly) once we sort it out. +pub fn mark_as_initialized_public(context: &mut PublicContext) { + let init_nullifier = compute_unsiloed_contract_initialization_nullifier(*context); + context.push_new_nullifier(init_nullifier, 0); +} + pub fn assert_is_initialized(context: &mut TContext) where TContext: ContextInterface { let init_nullifier = compute_contract_initialization_nullifier(*context); prove_nullifier_inclusion(init_nullifier, *context); diff --git a/noir-projects/noir-contracts/contracts/stateful_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/stateful_test_contract/src/main.nr index 6e51a861eea7..242e6f7f0167 100644 --- a/noir-projects/noir-contracts/contracts/stateful_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/stateful_test_contract/src/main.nr @@ -20,6 +20,13 @@ contract StatefulTest { let _res = context.call_private_function(context.this_address(), selector, [owner.to_field(), value]); } + #[aztec(public)] + #[aztec(initializer)] + fn public_constructor(owner: AztecAddress, value: Field) { + let selector = FunctionSelector::from_signature("increment_public_value_no_init_check((Field),Field)"); + let _res = context.call_public_function(context.this_address(), selector, [owner.to_field(), value]); + } + #[aztec(private)] fn create_note(owner: AztecAddress, value: Field) { if (value != 0) { diff --git a/noir/noir-repo/aztec_macros/src/lib.rs b/noir/noir-repo/aztec_macros/src/lib.rs index 7d943c90920d..9afedd39245e 100644 --- a/noir/noir-repo/aztec_macros/src/lib.rs +++ b/noir/noir-repo/aztec_macros/src/lib.rs @@ -711,16 +711,7 @@ fn transform_function( // Before returning mark the contract as initialized if is_initializer { - if ty == "Public" { - let error = AztecMacroError::UnsupportedAttributes { - span: func.def.name.span(), - secondary_message: Some( - "public functions cannot yet be used as initializers".to_owned(), - ), - }; - return Err(error); - } - let mark_initialized = create_mark_as_initialized(); + let mark_initialized = create_mark_as_initialized(ty); func.def.body.0.push(mark_initialized); } @@ -1179,9 +1170,10 @@ fn create_init_check() -> Statement { /// ```noir /// mark_as_initialized(&mut context); /// ``` -fn create_mark_as_initialized() -> Statement { +fn create_mark_as_initialized(ty: &str) -> Statement { + let name = if ty == "Public" { "mark_as_initialized_public" } else { "mark_as_initialized" }; make_statement(StatementKind::Expression(call( - variable_path(chained_dep!("aztec", "initializer", "mark_as_initialized")), + variable_path(chained_dep!("aztec", "initializer", name)), vec![mutable_reference("context")], ))) } 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 5dd25d224fce..99ff6665cd08 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 @@ -24,7 +24,7 @@ import { deployInstance, registerContractClass, } from '@aztec/aztec.js/deployment'; -import { ContractClassIdPreimage, Point, PublicKey } from '@aztec/circuits.js'; +import { ContractClassIdPreimage, Point } from '@aztec/circuits.js'; import { siloNullifier } from '@aztec/circuits.js/hash'; import { FunctionSelector, FunctionType } from '@aztec/foundation/abi'; import { StatefulTestContract } from '@aztec/noir-contracts.js'; @@ -258,10 +258,6 @@ describe('e2e_deploy_contract', () => { /nullifier witness not found/i, ); }); - - it('refuses to call a public function that requires initialization', async () => { - // TODO(@spalladino) - }); }); describe('registering a contract class', () => { @@ -309,16 +305,14 @@ describe('e2e_deploy_contract', () => { describe(`deploying a contract instance ${how}`, () => { let instance: ContractInstanceWithAddress; let initArgs: StatefulContractCtorArgs; - let publicKey: PublicKey; let contract: StatefulTestContract; - beforeAll(async () => { - initArgs = [accounts[0].address, 42]; + const deployInstance = async () => { + const initArgs = [accounts[0].address, 42] as StatefulContractCtorArgs; const salt = Fr.random(); const portalAddress = EthAddress.random(); - publicKey = Point.random(); - - instance = getContractInstanceFromDeployParams(artifact, initArgs, salt, publicKey, portalAddress); + const publicKey = Point.random(); + const instance = getContractInstanceFromDeployParams(artifact, initArgs, salt, publicKey, portalAddress); const { address, contractClassId } = instance; logger(`Deploying contract instance at ${address.toString()} class id ${contractClassId.toString()}`); await deployFn(instance); @@ -338,7 +332,12 @@ describe('e2e_deploy_contract', () => { publicKey, }); expect(registered.address).toEqual(instance.address); - contract = await StatefulTestContract.at(instance.address, wallet); + const contract = await StatefulTestContract.at(instance.address, wallet); + return { contract, initArgs, instance, publicKey }; + }; + + beforeAll(async () => { + ({ instance, initArgs, contract } = await deployInstance()); }, 60_000); it('stores contract instance in the aztec node', async () => { @@ -381,6 +380,32 @@ describe('e2e_deploy_contract', () => { const stored = await contract.methods.get_public_value(whom).view(); expect(stored).toEqual(10n); }, 30_000); + + it('refuses to reinitialize the contract', async () => { + await expect( + contract.methods + .public_constructor(...initArgs) + .send({ skipPublicSimulation: true }) + .wait(), + ).rejects.toThrow(/dropped/i); + }, 30_000); + + it('initializes a new instance of the contract via a public function', async () => { + const { contract, initArgs } = await deployInstance(); + const whom = initArgs[0]; + logger.info(`Initializing contract at ${contract.address} via a public function`); + await contract.methods + .public_constructor(...initArgs) + .send({ skipPublicSimulation: true }) + .wait(); + expect(await contract.methods.get_public_value(whom).view()).toEqual(42n); + logger.info(`Calling a public function that requires initialization on ${contract.address}`); + await contract.methods.increment_public_value(whom, 10).send().wait(); + expect(await contract.methods.get_public_value(whom).view()).toEqual(52n); + logger.info(`Calling a private function that requires initialization on ${contract.address}`); + await contract.methods.create_note(whom, 10).send().wait(); + expect(await contract.methods.summed_values(whom).view()).toEqual(10n); + }, 90_000); }); testDeployingAnInstance('from a wallet', async instance => {