diff --git a/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts b/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts index 0b0f69d451f..22d3d9dd04c 100644 --- a/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts @@ -162,6 +162,7 @@ export class Oracle { sortOrder: ACVMField[], [limit]: ACVMField[], [offset]: ACVMField[], + [status]: ACVMField[], [returnSize]: ACVMField[], ): Promise { const noteDatas = await this.typedOracle.getNotes( @@ -174,6 +175,7 @@ export class Oracle { sortOrder.map(s => +s), +limit, +offset, + +status, ); const noteLength = noteDatas?.[0]?.note.items.length ?? 0; diff --git a/yarn-project/acir-simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/acir-simulator/src/acvm/oracle/typed_oracle.ts index dfb81722a41..ffa9ac1fbfc 100644 --- a/yarn-project/acir-simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/acir-simulator/src/acvm/oracle/typed_oracle.ts @@ -2,6 +2,7 @@ import { CompleteAddress, MerkleTreeId, Note, + NoteStatus, NullifierMembershipWitness, PublicDataWitness, PublicKey, @@ -137,6 +138,7 @@ export abstract class TypedOracle { _sortOrder: number[], _limit: number, _offset: number, + _status: NoteStatus, ): Promise { throw new Error('Not available.'); } diff --git a/yarn-project/acir-simulator/src/client/client_execution_context.ts b/yarn-project/acir-simulator/src/client/client_execution_context.ts index 29ee3303683..7775f8dfd9f 100644 --- a/yarn-project/acir-simulator/src/client/client_execution_context.ts +++ b/yarn-project/acir-simulator/src/client/client_execution_context.ts @@ -1,4 +1,4 @@ -import { AuthWitness, FunctionL2Logs, L1NotePayload, Note, UnencryptedL2Log } from '@aztec/circuit-types'; +import { AuthWitness, FunctionL2Logs, L1NotePayload, Note, NoteStatus, UnencryptedL2Log } from '@aztec/circuit-types'; import { CallContext, ContractDeploymentData, @@ -190,6 +190,7 @@ export class ClientExecutionContext extends ViewDataOracle { * @param sortOrder - The order of the corresponding index in sortBy. (1: DESC, 2: ASC, 0: Do nothing) * @param limit - The number of notes to retrieve per query. * @param offset - The starting index for pagination. + * @param status - The status of notes to fetch. * @returns Array of note data. */ public async getNotes( @@ -202,12 +203,13 @@ export class ClientExecutionContext extends ViewDataOracle { sortOrder: number[], limit: number, offset: number, + status: NoteStatus, ): Promise { // Nullified pending notes are already removed from the list. const pendingNotes = this.noteCache.getNotes(this.contractAddress, storageSlot); const pendingNullifiers = this.noteCache.getNullifiers(this.contractAddress); - const dbNotes = await this.db.getNotes(this.contractAddress, storageSlot); + const dbNotes = await this.db.getNotes(this.contractAddress, storageSlot, status); const dbNotesFiltered = dbNotes.filter(n => !pendingNullifiers.has((n.siloedNullifier as Fr).value)); const notes = pickNotes([...dbNotesFiltered, ...pendingNotes], { diff --git a/yarn-project/acir-simulator/src/client/db_oracle.ts b/yarn-project/acir-simulator/src/client/db_oracle.ts index c4cfeedbfdf..cccf02838b5 100644 --- a/yarn-project/acir-simulator/src/client/db_oracle.ts +++ b/yarn-project/acir-simulator/src/client/db_oracle.ts @@ -1,4 +1,4 @@ -import { L2Block, MerkleTreeId, NullifierMembershipWitness, PublicDataWitness } from '@aztec/circuit-types'; +import { L2Block, MerkleTreeId, NoteStatus, NullifierMembershipWitness, PublicDataWitness } from '@aztec/circuit-types'; import { CompleteAddress, Header } from '@aztec/circuits.js'; import { FunctionArtifactWithDebugMetadata, FunctionSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; @@ -61,9 +61,10 @@ export interface DBOracle extends CommitmentsDB { * * @param contractAddress - The AztecAddress instance representing the contract address. * @param storageSlot - The Fr instance representing the storage slot of the notes. + * @param status - The status of notes to fetch. * @returns A Promise that resolves to an array of note data. */ - getNotes(contractAddress: AztecAddress, storageSlot: Fr): Promise; + getNotes(contractAddress: AztecAddress, storageSlot: Fr, status: NoteStatus): Promise; /** * Retrieve the artifact information of a specific function within a contract. diff --git a/yarn-project/acir-simulator/src/client/view_data_oracle.ts b/yarn-project/acir-simulator/src/client/view_data_oracle.ts index 191bb7b5b66..2a908670f6e 100644 --- a/yarn-project/acir-simulator/src/client/view_data_oracle.ts +++ b/yarn-project/acir-simulator/src/client/view_data_oracle.ts @@ -3,6 +3,7 @@ import { AztecNode, CompleteAddress, MerkleTreeId, + NoteStatus, NullifierMembershipWitness, PublicDataWitness, } from '@aztec/circuit-types'; @@ -169,6 +170,7 @@ export class ViewDataOracle extends TypedOracle { * @param sortOrder - The order of the corresponding index in sortBy. (1: DESC, 2: ASC, 0: Do nothing) * @param limit - The number of notes to retrieve per query. * @param offset - The starting index for pagination. + * @param status - The status of notes to fetch. * @returns Array of note data. */ public async getNotes( @@ -181,8 +183,9 @@ export class ViewDataOracle extends TypedOracle { sortOrder: number[], limit: number, offset: number, + status: NoteStatus, ): Promise { - const dbNotes = await this.db.getNotes(this.contractAddress, storageSlot); + const dbNotes = await this.db.getNotes(this.contractAddress, storageSlot, status); return pickNotes(dbNotes, { selects: selectBy .slice(0, numSelects) diff --git a/yarn-project/aztec-nr/aztec/src/note/note_getter.nr b/yarn-project/aztec-nr/aztec/src/note/note_getter.nr index 0a5420ff7ba..441f77f0d1f 100644 --- a/yarn-project/aztec-nr/aztec/src/note/note_getter.nr +++ b/yarn-project/aztec-nr/aztec/src/note/note_getter.nr @@ -8,7 +8,7 @@ use dep::protocol_types::constants::{ }; use crate::context::PrivateContext; use crate::note::{ - note_getter_options::{NoteGetterOptions, Select, Sort, SortOrder, Comparator}, + note_getter_options::{NoteGetterOptions, Select, Sort, SortOrder, Comparator, NoteStatus}, note_interface::NoteInterface, note_viewer_options::NoteViewerOptions, utils::compute_note_hash_for_read_or_nullify, @@ -135,6 +135,7 @@ unconstrained fn get_note_internal(storage_slot: Field, note_interface: [], 1, // limit 0, // offset + NoteStatus.ACTIVE, placeholder_note, placeholder_fields )[0].unwrap() // Notice: we don't allow dummies to be returned from get_note (singular). @@ -159,6 +160,7 @@ unconstrained fn get_notes_internal( sort_order, options.limit, options.offset, + options.status, placeholder_opt_notes, placeholder_fields ); @@ -187,6 +189,7 @@ unconstrained pub fn view_notes( sort_order, options.limit, options.offset, + options.status, placeholder_opt_notes, placeholder_fields ) diff --git a/yarn-project/aztec-nr/aztec/src/note/note_getter_options.nr b/yarn-project/aztec-nr/aztec/src/note/note_getter_options.nr index 78fbeffd553..3010135ebb3 100644 --- a/yarn-project/aztec-nr/aztec/src/note/note_getter_options.nr +++ b/yarn-project/aztec-nr/aztec/src/note/note_getter_options.nr @@ -53,6 +53,17 @@ impl Sort { } } +struct NoteStatusEnum { + ACTIVE: u2, + ACTIVE_OR_NULLIFIED: u2, +} + +global NoteStatus = NoteStatusEnum { + ACTIVE: 1, + ACTIVE_OR_NULLIFIED: 2, + // TODO 4217: add 'NULLIFIED' +}; + fn return_all_notes( notes: [Option; MAX_READ_REQUESTS_PER_CALL], _p: Field @@ -68,6 +79,7 @@ struct NoteGetterOptions { offset: u32, filter: fn ([Option; MAX_READ_REQUESTS_PER_CALL], FILTER_ARGS) -> [Option; MAX_READ_REQUESTS_PER_CALL], filter_args: FILTER_ARGS, + status: u2, } // docs:end:NoteGetterOptions @@ -85,6 +97,7 @@ impl NoteGetterOptions { offset: 0, filter: return_all_notes, filter_args: 0, + status: NoteStatus.ACTIVE, } } @@ -101,6 +114,7 @@ impl NoteGetterOptions { offset: 0, filter, filter_args, + status: NoteStatus.ACTIVE, } } @@ -120,16 +134,22 @@ impl NoteGetterOptions { *self } - // This method lets you set a limit for the maximum number of notes to be retrieved in a single query result. + // This method lets you set a limit for the maximum number of notes to be retrieved in a single query result. pub fn set_limit(&mut self, limit: u32) -> Self { assert(limit <= MAX_READ_REQUESTS_PER_CALL as u32); self.limit = limit; *self } - // This method sets the offset value, which determines where to start retrieving notes in the query results. + // This method sets the offset value, which determines where to start retrieving notes in the query results. pub fn set_offset(&mut self, offset: u32) -> Self { self.offset = offset; *self } + + // This method sets the status value, which determines whether to retrieve active or nullified notes. + pub fn set_status(&mut self, status: u2) -> Self { + self.status = status; + *self + } } diff --git a/yarn-project/aztec-nr/aztec/src/note/note_viewer_options.nr b/yarn-project/aztec-nr/aztec/src/note/note_viewer_options.nr index 34c7271bfeb..4b1c8949f3c 100644 --- a/yarn-project/aztec-nr/aztec/src/note/note_viewer_options.nr +++ b/yarn-project/aztec-nr/aztec/src/note/note_viewer_options.nr @@ -1,6 +1,6 @@ use dep::std::option::Option; use dep::protocol_types::constants::MAX_NOTES_PER_PAGE; -use crate::note::note_getter_options::{Select, Sort, Comparator}; +use crate::note::note_getter_options::{Select, Sort, Comparator, NoteStatus}; use crate::types::vec::BoundedVec; // docs:start:NoteViewerOptions @@ -9,6 +9,7 @@ struct NoteViewerOptions { sorts: BoundedVec, N>, limit: u32, offset: u32, + status: u2, } // docs:end:NoteViewerOptions @@ -19,12 +20,13 @@ impl NoteViewerOptions { sorts: BoundedVec::new(Option::none()), limit: MAX_NOTES_PER_PAGE as u32, offset: 0, + status: NoteStatus.ACTIVE, } } - + // This method adds a `Select` criterion to the options. - // It takes a field_index indicating which field to select, - // a value representing the specific value to match in that field, and + // It takes a field_index indicating which field to select, + // a value representing the specific value to match in that field, and // a comparator (For possible values of comparators, please see the Comparator enum from note_getter_options) pub fn select(&mut self, field_index: u8, value: Field, comparator: Option) -> Self { self.selects.push(Option::some(Select::new(field_index, value, comparator.unwrap_or(Comparator.EQ)))); @@ -46,4 +48,10 @@ impl NoteViewerOptions { self.offset = offset; *self } + + // This method sets the status value, which determines whether to retrieve active or nullified notes. + pub fn set_status(&mut self, status: u2) -> Self { + self.status = status; + *self + } } diff --git a/yarn-project/aztec-nr/aztec/src/oracle/notes.nr b/yarn-project/aztec-nr/aztec/src/oracle/notes.nr index 4176e647578..c4f2940f3a2 100644 --- a/yarn-project/aztec-nr/aztec/src/oracle/notes.nr +++ b/yarn-project/aztec-nr/aztec/src/oracle/notes.nr @@ -32,6 +32,7 @@ fn get_notes_oracle( _sort_order: [u2; N], _limit: u32, _offset: u32, + _status: u2, _return_size: u32, _placeholder_fields: [Field; S] ) -> [Field; S] {} @@ -46,6 +47,7 @@ unconstrained fn get_notes_oracle_wrapper( sort_order: [u2; N], limit: u32, offset: u32, + status: u2, mut placeholder_fields: [Field; S] ) -> [Field; S] { let return_size = placeholder_fields.len() as u32; @@ -59,6 +61,7 @@ unconstrained fn get_notes_oracle_wrapper( sort_order, limit, offset, + status, return_size, placeholder_fields ) @@ -75,6 +78,7 @@ unconstrained pub fn get_notes( sort_order: [u2; M], limit: u32, offset: u32, + status: u2, mut placeholder_opt_notes: [Option; S], // TODO: Remove it and use `limit` to initialize the note array. placeholder_fields: [Field; NS] // TODO: Remove it and use `limit` to initialize the note array. ) -> [Option; S] { @@ -88,6 +92,7 @@ unconstrained pub fn get_notes( sort_order, limit, offset, + status, placeholder_fields ); let num_notes = fields[0] as u32; diff --git a/yarn-project/circuit-types/src/notes/note_filter.ts b/yarn-project/circuit-types/src/notes/note_filter.ts index 362526d13ff..9437e8866c1 100644 --- a/yarn-project/circuit-types/src/notes/note_filter.ts +++ b/yarn-project/circuit-types/src/notes/note_filter.ts @@ -2,6 +2,15 @@ import { AztecAddress, Fr } from '@aztec/circuits.js'; import { TxHash } from '../index.js'; +/** + * The status of notes to retrieve. + */ +export enum NoteStatus { + ACTIVE = 1, + ACTIVE_OR_NULLIFIED = 2, + // TODO 4217: add 'NULLIFIED' +} + /** * A filter used to fetch Notes. * @remarks This filter is applied as an intersection of all it's params. @@ -15,6 +24,8 @@ export type NoteFilter = { storageSlot?: Fr; /** The owner of the note (whose public key was used to encrypt the note). */ owner?: AztecAddress; + /** The status of the note. Defaults to 'ACTIVE'. */ + status?: NoteStatus; }; /** diff --git a/yarn-project/end-to-end/src/e2e_note_getter.test.ts b/yarn-project/end-to-end/src/e2e_note_getter.test.ts index 7f5f7788e90..ed00016612a 100644 --- a/yarn-project/end-to-end/src/e2e_note_getter.test.ts +++ b/yarn-project/end-to-end/src/e2e_note_getter.test.ts @@ -1,5 +1,5 @@ -import { Comparator, Fr, Wallet } from '@aztec/aztec.js'; -import { DocsExampleContract } from '@aztec/noir-contracts'; +import { AztecAddress, Comparator, Fr, Wallet, toBigInt } from '@aztec/aztec.js'; +import { DocsExampleContract, TestContract } from '@aztec/noir-contracts'; import { setup } from './fixtures/utils.js'; @@ -17,128 +17,241 @@ function unwrapOptions(options: NoirOption[]): T[] { describe('e2e_note_getter', () => { let wallet: Wallet; - let teardown: () => Promise; - let contract: DocsExampleContract; beforeAll(async () => { ({ teardown, wallet } = await setup()); - contract = await DocsExampleContract.deploy(wallet).send().deployed(); - // sets card value to 1 and leader to sender. - await contract.methods.initialize_private(Fr.random(), 1).send().wait(); }, 25_000); afterAll(() => teardown()); - it('inserts notes from 0-9, then makes multiple queries specifying the total suite of comparators', async () => { - // ISSUE #4243 - // Calling this function does not work like this - // const numbers = [...Array(10).keys()]; - // await Promise.all(numbers.map(number => contract.methods.insert_note(number).send().wait())); - // It causes a race condition complaining about root mismatch - - await contract.methods - .insert_notes([...Array(10).keys()]) - .send() - .wait(); - await contract.methods.insert_note(5, Fr.ZERO).send().wait(); - - const [returnEq, returnNeq, returnLt, returnGt, returnLte, returnGte] = await Promise.all([ - contract.methods.read_note(5, Comparator.EQ).view(), - contract.methods.read_note(5, Comparator.NEQ).view(), - contract.methods.read_note(5, Comparator.LT).view(), - contract.methods.read_note(5, Comparator.GT).view(), - contract.methods.read_note(5, Comparator.LTE).view(), - // docs:start:state_vars-NoteGetterOptionsComparatorExampleTs - contract.methods.read_note(5, Comparator.GTE).view(), - // docs:end:state_vars-NoteGetterOptionsComparatorExampleTs - ]); - - expect( - unwrapOptions(returnEq) - .map(({ points, randomness }: any) => ({ points, randomness })) - .sort(sortFunc), - ).toStrictEqual( - [ - { points: 5n, randomness: 1n }, - { points: 5n, randomness: 0n }, - ].sort(sortFunc), - ); - - expect( - unwrapOptions(returnNeq) - .map(({ points, randomness }: any) => ({ points, randomness })) - .sort(sortFunc), - ).toStrictEqual( - [ - { points: 0n, randomness: 1n }, - { points: 1n, randomness: 1n }, - { points: 7n, randomness: 1n }, - { points: 9n, randomness: 1n }, - { points: 2n, randomness: 1n }, - { points: 6n, randomness: 1n }, - { points: 8n, randomness: 1n }, - { points: 4n, randomness: 1n }, - { points: 3n, randomness: 1n }, - ].sort(sortFunc), - ); - - expect( - unwrapOptions(returnLt) - .map(({ points, randomness }: any) => ({ points, randomness })) - .sort(sortFunc), - ).toStrictEqual( - [ - { points: 0n, randomness: 1n }, - { points: 1n, randomness: 1n }, - { points: 2n, randomness: 1n }, - { points: 4n, randomness: 1n }, - { points: 3n, randomness: 1n }, - ].sort(sortFunc), - ); - - expect( - unwrapOptions(returnGt) - .map(({ points, randomness }: any) => ({ points, randomness })) - .sort(sortFunc), - ).toStrictEqual( - [ - { points: 7n, randomness: 1n }, - { points: 9n, randomness: 1n }, - { points: 6n, randomness: 1n }, - { points: 8n, randomness: 1n }, - ].sort(sortFunc), - ); - - expect( - unwrapOptions(returnLte) - .map(({ points, randomness }: any) => ({ points, randomness })) - .sort(sortFunc), - ).toStrictEqual( - [ - { points: 5n, randomness: 1n }, - { points: 5n, randomness: 0n }, - { points: 0n, randomness: 1n }, - { points: 1n, randomness: 1n }, - { points: 2n, randomness: 1n }, - { points: 4n, randomness: 1n }, - { points: 3n, randomness: 1n }, - ].sort(sortFunc), - ); - - expect( - unwrapOptions(returnGte) - .map(({ points, randomness }: any) => ({ points, randomness })) - .sort(sortFunc), - ).toStrictEqual( - [ - { points: 5n, randomness: 0n }, - { points: 5n, randomness: 1n }, - { points: 7n, randomness: 1n }, - { points: 9n, randomness: 1n }, - { points: 6n, randomness: 1n }, - { points: 8n, randomness: 1n }, - ].sort(sortFunc), - ); - }, 300_000); + describe('comparators', () => { + let contract: DocsExampleContract; + + beforeAll(async () => { + contract = await DocsExampleContract.deploy(wallet).send().deployed(); + // sets card value to 1 and leader to sender. + await contract.methods.initialize_private(Fr.random(), 1).send().wait(); + }, 25_000); + + it('inserts notes from 0-9, then makes multiple queries specifying the total suite of comparators', async () => { + // ISSUE #4243 + // Calling this function does not work like this + // const numbers = [...Array(10).keys()]; + // await Promise.all(numbers.map(number => contract.methods.insert_note(number).send().wait())); + // It causes a race condition complaining about root mismatch + + await contract.methods + .insert_notes([...Array(10).keys()]) + .send() + .wait(); + await contract.methods.insert_note(5, Fr.ZERO).send().wait(); + + const [returnEq, returnNeq, returnLt, returnGt, returnLte, returnGte] = await Promise.all([ + contract.methods.read_note(5, Comparator.EQ).view(), + contract.methods.read_note(5, Comparator.NEQ).view(), + contract.methods.read_note(5, Comparator.LT).view(), + contract.methods.read_note(5, Comparator.GT).view(), + contract.methods.read_note(5, Comparator.LTE).view(), + // docs:start:state_vars-NoteGetterOptionsComparatorExampleTs + contract.methods.read_note(5, Comparator.GTE).view(), + // docs:end:state_vars-NoteGetterOptionsComparatorExampleTs + ]); + + expect( + unwrapOptions(returnEq) + .map(({ points, randomness }: any) => ({ points, randomness })) + .sort(sortFunc), + ).toStrictEqual( + [ + { points: 5n, randomness: 1n }, + { points: 5n, randomness: 0n }, + ].sort(sortFunc), + ); + + expect( + unwrapOptions(returnNeq) + .map(({ points, randomness }: any) => ({ points, randomness })) + .sort(sortFunc), + ).toStrictEqual( + [ + { points: 0n, randomness: 1n }, + { points: 1n, randomness: 1n }, + { points: 7n, randomness: 1n }, + { points: 9n, randomness: 1n }, + { points: 2n, randomness: 1n }, + { points: 6n, randomness: 1n }, + { points: 8n, randomness: 1n }, + { points: 4n, randomness: 1n }, + { points: 3n, randomness: 1n }, + ].sort(sortFunc), + ); + + expect( + unwrapOptions(returnLt) + .map(({ points, randomness }: any) => ({ points, randomness })) + .sort(sortFunc), + ).toStrictEqual( + [ + { points: 0n, randomness: 1n }, + { points: 1n, randomness: 1n }, + { points: 2n, randomness: 1n }, + { points: 4n, randomness: 1n }, + { points: 3n, randomness: 1n }, + ].sort(sortFunc), + ); + + expect( + unwrapOptions(returnGt) + .map(({ points, randomness }: any) => ({ points, randomness })) + .sort(sortFunc), + ).toStrictEqual( + [ + { points: 7n, randomness: 1n }, + { points: 9n, randomness: 1n }, + { points: 6n, randomness: 1n }, + { points: 8n, randomness: 1n }, + ].sort(sortFunc), + ); + + expect( + unwrapOptions(returnLte) + .map(({ points, randomness }: any) => ({ points, randomness })) + .sort(sortFunc), + ).toStrictEqual( + [ + { points: 5n, randomness: 1n }, + { points: 5n, randomness: 0n }, + { points: 0n, randomness: 1n }, + { points: 1n, randomness: 1n }, + { points: 2n, randomness: 1n }, + { points: 4n, randomness: 1n }, + { points: 3n, randomness: 1n }, + ].sort(sortFunc), + ); + + expect( + unwrapOptions(returnGte) + .map(({ points, randomness }: any) => ({ points, randomness })) + .sort(sortFunc), + ).toStrictEqual( + [ + { points: 5n, randomness: 0n }, + { points: 5n, randomness: 1n }, + { points: 7n, randomness: 1n }, + { points: 9n, randomness: 1n }, + { points: 6n, randomness: 1n }, + { points: 8n, randomness: 1n }, + ].sort(sortFunc), + ); + }, 300_000); + }); + + describe('status filter', () => { + let contract: TestContract; + let owner: AztecAddress; + + beforeAll(async () => { + contract = await TestContract.deploy(wallet).send().deployed(); + owner = wallet.getCompleteAddress().address; + }, 100_000); + + const VALUE = 5; + + // To prevent tests from interacting with one another, we'll have each use a different storage slot. + let storageSlot: number = 2; + + beforeEach(() => { + storageSlot += 1; + }); + + async function assertNoteIsReturned(storageSlot: number, expectedValue: number, activeOrNullified: boolean) { + const viewNotesResult = await contract.methods.call_view_notes(storageSlot, activeOrNullified).view(); + const getNotesResult = await callGetNotes(storageSlot, activeOrNullified); + + expect(viewNotesResult).toEqual(getNotesResult); + expect(viewNotesResult).toEqual(BigInt(expectedValue)); + } + + async function assertNoReturnValue(storageSlot: number, activeOrNullified: boolean) { + await expect(contract.methods.call_view_notes(storageSlot, activeOrNullified).view()).rejects.toThrow('is_some'); + await expect(contract.methods.call_get_notes(storageSlot, activeOrNullified).send().wait()).rejects.toThrow( + 'is_some', + ); + } + + async function callGetNotes(storageSlot: number, activeOrNullified: boolean): Promise { + // call_get_notes exposes the return value via an event since we cannot use view() with it. + const tx = contract.methods.call_get_notes(storageSlot, activeOrNullified).send(); + await tx.wait(); + + const logs = (await tx.getUnencryptedLogs()).logs; + expect(logs.length).toBe(1); + + return toBigInt(logs[0].log.data); + } + + async function callGetNotesMany(storageSlot: number, activeOrNullified: boolean): Promise> { + // call_get_notes_many exposes the return values via event since we cannot use view() with it. + const tx = contract.methods.call_get_notes_many(storageSlot, activeOrNullified).send(); + await tx.wait(); + + const logs = (await tx.getUnencryptedLogs()).logs; + expect(logs.length).toBe(2); + + return [toBigInt(logs[0].log.data), toBigInt(logs[1].log.data)]; + } + + describe('active note only', () => { + const activeOrNullified = false; + + it('returns active notes', async () => { + await contract.methods.call_create_note(VALUE, owner, storageSlot).send().wait(); + await assertNoteIsReturned(storageSlot, VALUE, activeOrNullified); + }, 30_000); + + it('does not return nullified notes', async () => { + await contract.methods.call_create_note(VALUE, owner, storageSlot).send().wait(); + await contract.methods.call_destroy_note(storageSlot).send().wait(); + + await assertNoReturnValue(storageSlot, activeOrNullified); + }, 30_000); + }); + + describe('active and nullified notes', () => { + const activeOrNullified = true; + + it('returns active notes', async () => { + await contract.methods.call_create_note(VALUE, owner, storageSlot).send().wait(); + await assertNoteIsReturned(storageSlot, VALUE, activeOrNullified); + }, 30_000); + + it('returns nullified notes', async () => { + await contract.methods.call_create_note(VALUE, owner, storageSlot).send().wait(); + await contract.methods.call_destroy_note(storageSlot).send().wait(); + + await assertNoteIsReturned(storageSlot, VALUE, activeOrNullified); + }, 30_000); + + it('returns both active and nullified notes', async () => { + // We store two notes with two different values in the same storage slot, and then delete one of them. Note that + // we can't be sure which one was deleted since we're just deleting based on the storage slot. + await contract.methods.call_create_note(VALUE, owner, storageSlot).send().wait(); + await contract.methods + .call_create_note(VALUE + 1, owner, storageSlot) + .send() + .wait(); + await contract.methods.call_destroy_note(storageSlot).send().wait(); + + // We now fetch multiple notes, and get both the active and the nullified one. + const viewNotesManyResult = await contract.methods.call_view_notes_many(storageSlot, activeOrNullified).view(); + const getNotesManyResult = await callGetNotesMany(storageSlot, activeOrNullified); + + // We can't be sure in which order the notes will be returned, so we simply sort them to test equality. Note + // however that both view_notes and get_notes get the exact same result. + expect(viewNotesManyResult).toEqual(getNotesManyResult); + expect(viewNotesManyResult.sort()).toEqual([BigInt(VALUE), BigInt(VALUE + 1)]); + }, 45_000); + }); + }); }); diff --git a/yarn-project/noir-contracts/contracts/test_contract/Nargo.toml b/yarn-project/noir-contracts/contracts/test_contract/Nargo.toml index 80a59556c0a..7ab24aa181e 100644 --- a/yarn-project/noir-contracts/contracts/test_contract/Nargo.toml +++ b/yarn-project/noir-contracts/contracts/test_contract/Nargo.toml @@ -7,4 +7,5 @@ type = "contract" [dependencies] aztec = { path = "../../../aztec-nr/aztec" } field_note = { path = "../../../aztec-nr/field-note" } +value_note = { path = "../../../aztec-nr/value-note" } token_portal_content_hash_lib = { path = "../token_portal_content_hash_lib" } diff --git a/yarn-project/noir-contracts/contracts/test_contract/src/main.nr b/yarn-project/noir-contracts/contracts/test_contract/src/main.nr index 6402ec444e8..11d0f4705f6 100644 --- a/yarn-project/noir-contracts/contracts/test_contract/src/main.nr +++ b/yarn-project/noir-contracts/contracts/test_contract/src/main.nr @@ -23,6 +23,10 @@ contract Test { note::{ note_header::NoteHeader, utils as note_utils, + lifecycle::{create_note, destroy_note}, + note_getter::{get_notes, view_notes}, + note_getter_options::{NoteGetterOptions, NoteStatus}, + note_viewer_options::NoteViewerOptions, }, oracle::{ get_public_key::get_public_key as get_public_key_oracle, @@ -35,6 +39,7 @@ contract Test { }; use dep::token_portal_content_hash_lib::{get_mint_private_content_hash, get_mint_public_content_hash}; use dep::field_note::field_note::{FieldNote, FieldNoteMethods, FIELD_NOTE_LEN}; + use dep::value_note::value_note::{ValueNote, ValueNoteMethods, VALUE_NOTE_LEN}; #[event] struct ExampleEvent { @@ -83,6 +88,89 @@ contract Test { context.this_address() } + #[aztec(private)] + fn call_create_note(value: Field, owner: AztecAddress, storage_slot: Field) { + assert(storage_slot != 1, "storage slot 1 is reserved for example_constant"); + + let mut note = ValueNote::new(value, owner); + create_note(&mut context, storage_slot, &mut note, ValueNoteMethods, true); + } + + #[aztec(private)] + fn call_get_notes(storage_slot: Field, active_or_nullified: bool) { + assert(storage_slot != 1, "storage slot 1 is reserved for example_constant"); + + let mut options = NoteGetterOptions::new(); + if (active_or_nullified) { + options = options.set_status(NoteStatus.ACTIVE_OR_NULLIFIED); + } + + let opt_notes = get_notes(&mut context, storage_slot, ValueNoteMethods, options); + + // We can't get the return value of a private function from the outside world in an end to end test, so we + // expose it via an unecrypted log instead. + let value = opt_notes[0].unwrap().value; + emit_unencrypted_log_from_private(&mut context, value); + } + + #[aztec(private)] + fn call_get_notes_many(storage_slot: Field, active_or_nullified: bool) { + assert(storage_slot != 1, "storage slot 1 is reserved for example_constant"); + + let mut options = NoteGetterOptions::new(); + if (active_or_nullified) { + options = options.set_status(NoteStatus.ACTIVE_OR_NULLIFIED); + } + + let opt_notes = get_notes(&mut context, storage_slot, ValueNoteMethods, options); + + // We can't get the return value of a private function from the outside world in an end to end test, so we + // expose it via an unecrypted log instead. + emit_unencrypted_log_from_private(&mut context, opt_notes[0].unwrap().value); + emit_unencrypted_log_from_private(&mut context, opt_notes[1].unwrap().value); + } + + unconstrained fn call_view_notes(storage_slot: Field, active_or_nullified: bool) -> pub Field { + assert(storage_slot != 1, "storage slot 1 is reserved for example_constant"); + + let mut options = NoteViewerOptions::new(); + if (active_or_nullified) { + options = options.set_status(NoteStatus.ACTIVE_OR_NULLIFIED); + } + + let opt_notes = view_notes(storage_slot, ValueNoteMethods, options); + + opt_notes[0].unwrap().value + } + + unconstrained fn call_view_notes_many( + storage_slot: Field, + active_or_nullified: bool + ) -> pub [Field; 2] { + assert(storage_slot != 1, "storage slot 1 is reserved for example_constant"); + + let mut options = NoteViewerOptions::new(); + if (active_or_nullified) { + options = options.set_status(NoteStatus.ACTIVE_OR_NULLIFIED); + } + + let opt_notes = view_notes(storage_slot, ValueNoteMethods, options); + + [opt_notes[0].unwrap().value, opt_notes[1].unwrap().value] + } + + #[aztec(private)] + fn call_destroy_note(storage_slot: Field) { + assert(storage_slot != 1, "storage slot 1 is reserved for example_constant"); + + let options = NoteGetterOptions::new(); + let opt_notes = get_notes(&mut context, storage_slot, ValueNoteMethods, options); + + let note = opt_notes[0].unwrap(); + + destroy_note(&mut context, note, ValueNoteMethods); + } + // Test codegen for Aztec.nr interfaces // See yarn-project/acir-simulator/src/client/private_execution.test.ts 'nested calls through autogenerated interface' // Note; this function is deliberately NOT annotated with #[aztec(private)] due to its use in tests @@ -238,10 +326,15 @@ contract Test { contract_address: AztecAddress, nonce: Field, storage_slot: Field, - serialized_note: [Field; FIELD_NOTE_LEN] + serialized_note: [Field; VALUE_NOTE_LEN] // must fit either a FieldNote or a ValueNote ) -> pub [Field; 4] { - assert(storage_slot == 1); - let note_header = NoteHeader::new(contract_address, nonce, storage_slot); - note_utils::compute_note_hash_and_nullifier(FieldNoteMethods, note_header, serialized_note) + if (storage_slot == 1) { + let note_header = NoteHeader::new(contract_address, nonce, storage_slot); + note_utils::compute_note_hash_and_nullifier(FieldNoteMethods, note_header, serialized_note) + } else { + // For ValueNotes created via write_value_to_storage + let note_header = NoteHeader::new(contract_address, nonce, storage_slot); + note_utils::compute_note_hash_and_nullifier(ValueNoteMethods, note_header, serialized_note) + } } } diff --git a/yarn-project/pxe/src/database/kv_pxe_database.ts b/yarn-project/pxe/src/database/kv_pxe_database.ts index 5392e7c0eee..15d93f80898 100644 --- a/yarn-project/pxe/src/database/kv_pxe_database.ts +++ b/yarn-project/pxe/src/database/kv_pxe_database.ts @@ -1,4 +1,4 @@ -import { ContractDao, MerkleTreeId, NoteFilter, PublicKey } from '@aztec/circuit-types'; +import { ContractDao, MerkleTreeId, NoteFilter, NoteStatus, PublicKey } from '@aztec/circuit-types'; import { AztecAddress, CompleteAddress, Header } from '@aztec/circuits.js'; import { ContractArtifact } from '@aztec/foundation/abi'; import { toBufferBE } from '@aztec/foundation/bigint-buffer'; @@ -22,11 +22,16 @@ export class KVPxeDatabase implements PxeDatabase { #capsules: AztecArray; #contracts: AztecMap; #notes: AztecMap; + #nullifiedNotes: AztecMap; #nullifierToNoteId: AztecMap; #notesByContract: AztecMultiMap; #notesByStorageSlot: AztecMultiMap; #notesByTxHash: AztecMultiMap; #notesByOwner: AztecMultiMap; + #nullifiedNotesByContract: AztecMultiMap; + #nullifiedNotesByStorageSlot: AztecMultiMap; + #nullifiedNotesByTxHash: AztecMultiMap; + #nullifiedNotesByOwner: AztecMultiMap; #deferredNotes: AztecArray; #deferredNotesByContract: AztecMultiMap; #syncedBlockPerPublicKey: AztecMap; @@ -46,22 +51,24 @@ export class KVPxeDatabase implements PxeDatabase { this.#contractArtifacts = db.openMap('contract_artifacts'); this.#contractInstances = db.openMap('contracts_instances'); - this.#notesByOwner = db.openMultiMap('notes_by_owner'); this.#synchronizedBlock = db.openSingleton('header'); this.#syncedBlockPerPublicKey = db.openMap('synced_block_per_public_key'); this.#notes = db.openMap('notes'); + this.#nullifiedNotes = db.openMap('nullified_notes'); this.#nullifierToNoteId = db.openMap('nullifier_to_note'); - this.#notesByContract = db.openMultiMap('notes_by_contract'); - this.#notesByStorageSlot = db.openMultiMap('notes_by_storage_slot'); - this.#notesByTxHash = db.openMultiMap('notes_by_tx_hash'); this.#notesByContract = db.openMultiMap('notes_by_contract'); this.#notesByStorageSlot = db.openMultiMap('notes_by_storage_slot'); this.#notesByTxHash = db.openMultiMap('notes_by_tx_hash'); this.#notesByOwner = db.openMultiMap('notes_by_owner'); + this.#nullifiedNotesByContract = db.openMultiMap('nullified_notes_by_contract'); + this.#nullifiedNotesByStorageSlot = db.openMultiMap('nullified_notes_by_storage_slot'); + this.#nullifiedNotesByTxHash = db.openMultiMap('nullified_notes_by_tx_hash'); + this.#nullifiedNotesByOwner = db.openMultiMap('nullified_notes_by_owner'); + this.#deferredNotes = db.openArray('deferred_notes'); this.#deferredNotesByContract = db.openMultiMap('deferred_notes_by_contract'); } @@ -181,46 +188,70 @@ export class KVPxeDatabase implements PxeDatabase { }); } - #getNotes(filter: NoteFilter = {}): NoteDao[] { + #getNotes(filter: NoteFilter): NoteDao[] { const publicKey: PublicKey | undefined = filter.owner ? this.#getCompleteAddress(filter.owner)?.publicKey : undefined; - const initialNoteIds = publicKey - ? this.#notesByOwner.getValues(publicKey.toString()) - : filter.txHash - ? this.#notesByTxHash.getValues(filter.txHash.toString()) - : filter.contractAddress - ? this.#notesByContract.getValues(filter.contractAddress.toString()) - : filter.storageSlot - ? this.#notesByStorageSlot.getValues(filter.storageSlot.toString()) - : this.#notes.keys(); + filter.status = filter.status ?? NoteStatus.ACTIVE; + + const candidateNoteSources = []; + + candidateNoteSources.push({ + ids: publicKey + ? this.#notesByOwner.getValues(publicKey.toString()) + : filter.txHash + ? this.#notesByTxHash.getValues(filter.txHash.toString()) + : filter.contractAddress + ? this.#notesByContract.getValues(filter.contractAddress.toString()) + : filter.storageSlot + ? this.#notesByStorageSlot.getValues(filter.storageSlot.toString()) + : this.#notes.keys(), + notes: this.#notes, + }); + + if (filter.status == NoteStatus.ACTIVE_OR_NULLIFIED) { + candidateNoteSources.push({ + ids: publicKey + ? this.#nullifiedNotesByOwner.getValues(publicKey.toString()) + : filter.txHash + ? this.#nullifiedNotesByTxHash.getValues(filter.txHash.toString()) + : filter.contractAddress + ? this.#nullifiedNotesByContract.getValues(filter.contractAddress.toString()) + : filter.storageSlot + ? this.#nullifiedNotesByStorageSlot.getValues(filter.storageSlot.toString()) + : this.#nullifiedNotes.keys(), + notes: this.#nullifiedNotes, + }); + } const result: NoteDao[] = []; - for (const noteId of initialNoteIds) { - const serializedNote = this.#notes.get(noteId); - if (!serializedNote) { - continue; - } + for (const { ids, notes } of candidateNoteSources) { + for (const id of ids) { + const serializedNote = notes.get(id); + if (!serializedNote) { + continue; + } - const note = NoteDao.fromBuffer(serializedNote); - if (filter.contractAddress && !note.contractAddress.equals(filter.contractAddress)) { - continue; - } + const note = NoteDao.fromBuffer(serializedNote); + if (filter.contractAddress && !note.contractAddress.equals(filter.contractAddress)) { + continue; + } - if (filter.txHash && !note.txHash.equals(filter.txHash)) { - continue; - } + if (filter.txHash && !note.txHash.equals(filter.txHash)) { + continue; + } - if (filter.storageSlot && !note.storageSlot.equals(filter.storageSlot!)) { - continue; - } + if (filter.storageSlot && !note.storageSlot.equals(filter.storageSlot!)) { + continue; + } - if (publicKey && !note.publicKey.equals(publicKey)) { - continue; - } + if (publicKey && !note.publicKey.equals(publicKey)) { + continue; + } - result.push(note); + result.push(note); + } } return result; @@ -265,6 +296,12 @@ export class KVPxeDatabase implements PxeDatabase { void this.#notesByContract.deleteValue(note.contractAddress.toString(), noteIndex); void this.#notesByStorageSlot.deleteValue(note.storageSlot.toString(), noteIndex); + void this.#nullifiedNotes.set(noteIndex, note.toBuffer()); + void this.#nullifiedNotesByContract.set(note.contractAddress.toString(), noteIndex); + void this.#nullifiedNotesByStorageSlot.set(note.storageSlot.toString(), noteIndex); + void this.#nullifiedNotesByTxHash.set(note.txHash.toString(), noteIndex); + void this.#nullifiedNotesByOwner.set(note.publicKey.toString(), noteIndex); + void this.#nullifierToNoteId.delete(nullifier.toString()); } diff --git a/yarn-project/pxe/src/database/pxe_database_test_suite.ts b/yarn-project/pxe/src/database/pxe_database_test_suite.ts index b5a3f4a13cf..0fceb543e42 100644 --- a/yarn-project/pxe/src/database/pxe_database_test_suite.ts +++ b/yarn-project/pxe/src/database/pxe_database_test_suite.ts @@ -1,4 +1,4 @@ -import { INITIAL_L2_BLOCK_NUM, NoteFilter, randomTxHash } from '@aztec/circuit-types'; +import { INITIAL_L2_BLOCK_NUM, NoteFilter, NoteStatus, randomTxHash } from '@aztec/circuit-types'; import { AztecAddress, CompleteAddress } from '@aztec/circuits.js'; import { makeHeader } from '@aztec/circuits.js/factories'; import { Fr, Point } from '@aztec/foundation/fields'; @@ -136,21 +136,53 @@ export function describePxeDatabase(getDatabase: () => PxeDatabase) { await expect(database.getNotes(getFilter())).resolves.toEqual(getExpected()); }); - it('removes nullified notes', async () => { + it.each(filteringTests)('retrieves nullified notes', async (getFilter, getExpected) => { + await database.addNotes(notes); + + // Nullify all notes and use the same filter as other test cases + for (const owner of owners) { + const notesToNullify = notes.filter(note => note.publicKey.equals(owner.publicKey)); + const nullifiers = notesToNullify.map(note => note.siloedNullifier); + await expect(database.removeNullifiedNotes(nullifiers, owner.publicKey)).resolves.toEqual(notesToNullify); + } + + await expect(database.getNotes({ ...getFilter(), status: NoteStatus.ACTIVE_OR_NULLIFIED })).resolves.toEqual( + getExpected(), + ); + }); + + it('skips nullified notes by default or when requesting active', async () => { + await database.addNotes(notes); + const notesToNullify = notes.filter(note => note.publicKey.equals(owners[0].publicKey)); const nullifiers = notesToNullify.map(note => note.siloedNullifier); + await expect(database.removeNullifiedNotes(nullifiers, notesToNullify[0].publicKey)).resolves.toEqual( + notesToNullify, + ); + + const actualNotesWithDefault = await database.getNotes({}); + const actualNotesWithActive = await database.getNotes({ status: NoteStatus.ACTIVE }); + + expect(actualNotesWithDefault).toEqual(actualNotesWithActive); + expect(actualNotesWithActive).toEqual(notes.filter(note => !notesToNullify.includes(note))); + }); + it('returns active and nullified notes when requesting either', async () => { await database.addNotes(notes); + const notesToNullify = notes.filter(note => note.publicKey.equals(owners[0].publicKey)); + const nullifiers = notesToNullify.map(note => note.siloedNullifier); await expect(database.removeNullifiedNotes(nullifiers, notesToNullify[0].publicKey)).resolves.toEqual( notesToNullify, ); - await expect( - database.getNotes({ - owner: owners[0].address, - }), - ).resolves.toEqual([]); - await expect(database.getNotes({})).resolves.toEqual(notes.filter(note => !notesToNullify.includes(note))); + + const result = await database.getNotes({ + status: NoteStatus.ACTIVE_OR_NULLIFIED, + }); + + // We have to compare the sorted arrays since the database does not return the same order as when originally + // inserted combining active and nullified results. + expect(result.sort()).toEqual([...notes].sort()); }); }); diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index dc7a2e703d6..a7b2a4dcd81 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -3,6 +3,7 @@ import { KeyStore, L2Block, MerkleTreeId, + NoteStatus, NullifierMembershipWitness, PublicDataWitness, StateInfoProvider, @@ -59,8 +60,12 @@ export class SimulatorOracle implements DBOracle { return capsule; } - async getNotes(contractAddress: AztecAddress, storageSlot: Fr) { - const noteDaos = await this.db.getNotes({ contractAddress, storageSlot }); + async getNotes(contractAddress: AztecAddress, storageSlot: Fr, status: NoteStatus) { + const noteDaos = await this.db.getNotes({ + contractAddress, + storageSlot, + status, + }); return noteDaos.map(({ contractAddress, storageSlot, nonce, note, innerNoteHash, siloedNullifier, index }) => ({ contractAddress, storageSlot,