From 4feaea59267437a0841aa14f445cee7556a0c0b4 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 28 Feb 2024 12:26:11 -0300 Subject: [PATCH] feat: Add aztec-nr private functions for initialization nullifier (#4807) Adds helper functions for emitting and checking an initialization nullifier in private-land. --- .../aztec-nr/aztec/src/initializer.nr | 29 ++++++++++++++++ noir-projects/aztec-nr/aztec/src/lib.nr | 1 + .../stateful_test_contract/src/main.nr | 18 ++++++++-- .../circuits.js/src/structs/side_effects.ts | 4 +++ .../src/e2e_deploy_contract.test.ts | 33 +++++++++++++++++++ .../pxe/src/kernel_prover/kernel_prover.ts | 2 +- .../src/client/private_execution.test.ts | 3 +- 7 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/initializer.nr diff --git a/noir-projects/aztec-nr/aztec/src/initializer.nr b/noir-projects/aztec-nr/aztec/src/initializer.nr new file mode 100644 index 00000000000..ac7fff02522 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/initializer.nr @@ -0,0 +1,29 @@ +use dep::protocol_types::hash::silo_nullifier; +use crate::context::PrivateContext; + +pub fn mark_as_initialized(context: &mut PrivateContext) { + let init_nullifier = compute_unsiloed_contract_initialization_nullifier(context); + context.push_new_nullifier(init_nullifier, 0); + + // We push a commitment as well and use this value to check initialization, + // since we cannot yet read a nullifier from the same tx in which it was emitted. + // Eventually, when that's supported, we should delete this note_hash and + // have all checks rely on reading the nullifier directly. + // TODO(@spalladino) Remove when possible. + context.push_new_note_hash(init_nullifier); +} + +// TODO(@spalladino): Add a variant using PublicContext once we can read nullifiers or note hashes from public-land. +pub fn assert_is_initialized(context: &mut PrivateContext) { + let init_nullifier = compute_contract_initialization_nullifier(context); + context.push_read_request(init_nullifier); +} + +pub fn compute_contract_initialization_nullifier(context: &mut PrivateContext) -> Field { + let address = context.this_address(); + silo_nullifier(address, compute_unsiloed_contract_initialization_nullifier(context)) +} + +pub fn compute_unsiloed_contract_initialization_nullifier(context: &mut PrivateContext) -> Field { + context.this_address().to_field() +} diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index 49ecf959cb1..2d6eeb862c9 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -4,6 +4,7 @@ mod deploy; mod hash; mod hasher; mod history; +mod initializer; mod key; mod log; mod messaging; 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 42637e67d35..094556ac7a0 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 @@ -1,6 +1,5 @@ // 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, abis::function_selector::FunctionSelector}; use dep::std::option::Option; use dep::value_note::{balance_utils, utils::{increment, decrement}, value_note::{VALUE_NOTE_LEN, ValueNote}}; use dep::aztec::{ @@ -8,7 +7,9 @@ contract StatefulTest { context::{PrivateContext, PublicContext, Context}, note::{note_header::NoteHeader, utils as note_utils}, state_vars::{Map, PublicMutable, PrivateSet}, - oracle::get_contract_instance::get_contract_instance + oracle::get_contract_instance::get_contract_instance, + initializer::{mark_as_initialized, assert_is_initialized}, + protocol_types::{address::AztecAddress, abis::function_selector::FunctionSelector}, }; struct Storage { @@ -18,12 +19,22 @@ contract StatefulTest { #[aztec(private)] fn constructor(owner: AztecAddress, value: Field) { - let selector = FunctionSelector::from_signature("create_note((Field),Field)"); + let selector = FunctionSelector::from_signature("internal_create_note((Field),Field)"); let _res = context.call_private_function(context.this_address(), selector, [owner.to_field(), value]); + mark_as_initialized(&mut context); } #[aztec(private)] fn create_note(owner: AztecAddress, value: Field) { + assert_is_initialized(&mut context); + if (value != 0) { + let loc = storage.notes.at(owner); + increment(loc, value, owner); + } + } + + #[aztec(private)] + internal fn internal_create_note(owner: AztecAddress, value: Field) { if (value != 0) { let loc = storage.notes.at(owner); increment(loc, value, owner); @@ -32,6 +43,7 @@ contract StatefulTest { #[aztec(private)] fn destroy_and_create(recipient: AztecAddress, amount: Field) { + assert_is_initialized(&mut context); let sender = context.msg_sender(); let sender_notes = storage.notes.at(sender); diff --git a/yarn-project/circuits.js/src/structs/side_effects.ts b/yarn-project/circuits.js/src/structs/side_effects.ts index a39eb52420c..98fdaa715e9 100644 --- a/yarn-project/circuits.js/src/structs/side_effects.ts +++ b/yarn-project/circuits.js/src/structs/side_effects.ts @@ -33,6 +33,10 @@ export class SideEffect implements SideEffectType { public counter: Fr, ) {} + toString(): string { + return `value=${this.value.toString()} counter=${this.counter.toString()}`; + } + /** * Serialize this as a buffer. * @returns The buffer. 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 7c7247e9016..828b534a5ff 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 @@ -213,6 +213,39 @@ describe('e2e_deploy_contract', () => { expect(await contracts[0].methods.summed_values(owner).view()).toEqual(42n); expect(await contracts[1].methods.summed_values(owner).view()).toEqual(52n); }, 30_000); + + it('refuses to initialize a contract twice', async () => { + const owner = await registerRandomAccount(pxe); + const initArgs: StatefulContractCtorArgs = [owner, 42]; + const contract = await registerContract(wallet, StatefulTestContract, initArgs); + await contract.methods + .constructor(...initArgs) + .send() + .wait(); + await expect( + contract.methods + .constructor(...initArgs) + .send() + .wait(), + ).rejects.toThrow(/dropped/); + }); + + it('refuses to call a private function that requires initialization', async () => { + const owner = await registerRandomAccount(pxe); + const initArgs: StatefulContractCtorArgs = [owner, 42]; + const contract = await registerContract(wallet, StatefulTestContract, initArgs); + // TODO(@spalladino): It'd be nicer to be able to fail the assert with a more descriptive message, + // but the best we can do for now is pushing a read request to the kernel and wait for it to fail. + // Maybe we need an unconstrained check for the read request that runs within the app circuit simulation + // so we can bail earlier with a more descriptive error? I should create an issue for this. + await expect(contract.methods.create_note(owner, 10).send().wait()).rejects.toThrow( + /The read request.*does not match/, + ); + }); + + it('refuses to call a public function that requires initialization', async () => { + // TODO(@spalladino) + }); }); describe('registering a contract class', () => { diff --git a/yarn-project/pxe/src/kernel_prover/kernel_prover.ts b/yarn-project/pxe/src/kernel_prover/kernel_prover.ts index aa8aa944e63..5fc8f3412e3 100644 --- a/yarn-project/pxe/src/kernel_prover/kernel_prover.ts +++ b/yarn-project/pxe/src/kernel_prover/kernel_prover.ts @@ -337,7 +337,7 @@ export class KernelProver { const result = noteHashes.findIndex(equalToRR); if (result == -1) { throw new Error( - `The read request at index ${i} with value ${readRequests[i].toString()} does not match to any commitment.`, + `The read request at index ${i} ${readRequests[i].toString()} does not match to any commitment.`, ); } else { hints[i] = new Fr(result); diff --git a/yarn-project/simulator/src/client/private_execution.test.ts b/yarn-project/simulator/src/client/private_execution.test.ts index 2bd6f735c57..09d568bfd18 100644 --- a/yarn-project/simulator/src/client/private_execution.test.ts +++ b/yarn-project/simulator/src/client/private_execution.test.ts @@ -411,7 +411,8 @@ describe('Private Execution test suite', () => { expect(changeNote.note.items[0]).toEqual(new Fr(40n)); const readRequests = sideEffectArrayToValueArray( - nonEmptySideEffects(result.callStackItem.publicInputs.readRequests), + // We remove the first element which is the read request for the initialization commitment + nonEmptySideEffects(result.callStackItem.publicInputs.readRequests.slice(1)), ); expect(readRequests).toHaveLength(consumedNotes.length);