Skip to content

Commit

Permalink
feat: Enable public constructor functions (#4896)
Browse files Browse the repository at this point in the history
Allow decorating a public function as `initializer`
  • Loading branch information
spalladino committed Mar 5, 2024
1 parent 3747619 commit 7b06895
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 25 deletions.
10 changes: 9 additions & 1 deletion noir-projects/aztec-nr/aztec/src/initializer.nr
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
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) {
let init_nullifier = compute_unsiloed_contract_initialization_nullifier(*context);
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<TContext>(context: &mut TContext) where TContext: ContextInterface {
let init_nullifier = compute_contract_initialization_nullifier(*context);
prove_nullifier_inclusion(init_nullifier, *context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
16 changes: 4 additions & 12 deletions noir/noir-repo/aztec_macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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")],
)))
}
Expand Down
49 changes: 37 additions & 12 deletions yarn-project/end-to-end/src/e2e_deploy_contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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 => {
Expand Down

0 comments on commit 7b06895

Please sign in to comment.