diff --git a/barretenberg/cpp/src/barretenberg/common/fuzzer.hpp b/barretenberg/cpp/src/barretenberg/common/fuzzer.hpp index 9ffa30975f04..a22d8038ebae 100644 --- a/barretenberg/cpp/src/barretenberg/common/fuzzer.hpp +++ b/barretenberg/cpp/src/barretenberg/common/fuzzer.hpp @@ -89,10 +89,10 @@ class FastRandom { */ template concept SimpleRng = requires(T a) { - { - a.next() - } -> std::convertible_to; - }; + { + a.next() + } -> std::convertible_to; +}; /** * @brief Concept for forcing ArgumentSizes to be size_t * @@ -100,27 +100,27 @@ concept SimpleRng = requires(T a) { */ template concept InstructionArgumentSizes = requires { - { - std::make_tuple(T::CONSTANT, - T::WITNESS, - T::CONSTANT_WITNESS, - T::ADD, - T::SUBTRACT, - T::MULTIPLY, - T::DIVIDE, - T::ADD_TWO, - T::MADD, - T::MULT_MADD, - T::MSUB_DIV, - T::SQR, - T::SQR_ADD, - T::SUBTRACT_WITH_CONSTRAINT, - T::DIVIDE_WITH_CONSTRAINTS, - T::SLICE, - T::ASSERT_ZERO, - T::ASSERT_NOT_ZERO) - } -> std::same_as>; - }; + { + std::make_tuple(T::CONSTANT, + T::WITNESS, + T::CONSTANT_WITNESS, + T::ADD, + T::SUBTRACT, + T::MULTIPLY, + T::DIVIDE, + T::ADD_TWO, + T::MADD, + T::MULT_MADD, + T::MSUB_DIV, + T::SQR, + T::SQR_ADD, + T::SUBTRACT_WITH_CONSTRAINT, + T::DIVIDE_WITH_CONSTRAINTS, + T::SLICE, + T::ASSERT_ZERO, + T::ASSERT_NOT_ZERO) + } -> std::same_as>; +}; /** * @brief Concept for Havoc Configurations @@ -128,13 +128,12 @@ concept InstructionArgumentSizes = requires { * @tparam T */ template -concept HavocConfigConstraint = - requires { - { - std::make_tuple(T::GEN_MUTATION_COUNT_LOG, T::GEN_STRUCTURAL_MUTATION_PROBABILITY) - } -> std::same_as>; - T::GEN_MUTATION_COUNT_LOG <= 7; - }; +concept HavocConfigConstraint = requires { + { + std::make_tuple(T::GEN_MUTATION_COUNT_LOG, T::GEN_STRUCTURAL_MUTATION_PROBABILITY) + } -> std::same_as>; + T::GEN_MUTATION_COUNT_LOG <= 7; +}; /** * @brief Concept specifying the class used by the fuzzer * @@ -142,12 +141,12 @@ concept HavocConfigConstraint = */ template concept ArithmeticFuzzHelperConstraint = requires { - typename T::ArgSizes; - typename T::Instruction; - typename T::ExecutionState; - typename T::ExecutionHandler; - InstructionArgumentSizes; - }; + typename T::ArgSizes; + typename T::Instruction; + typename T::ExecutionState; + typename T::ExecutionHandler; + InstructionArgumentSizes; +}; /** * @brief Fuzzer uses only composers with check_circuit function @@ -156,10 +155,10 @@ concept ArithmeticFuzzHelperConstraint = requires { */ template concept CheckableComposer = requires(T a) { - { - a.check_circuit() - } -> std::same_as; - }; + { + a.check_circuit() + } -> std::same_as; +}; /** * @brief The fuzzer can use a postprocessing function that is specific to the type being fuzzed @@ -170,10 +169,10 @@ concept CheckableComposer = requires(T a) { */ template concept PostProcessingEnabled = requires(Composer composer, Context context) { - { - T::postProcess(&composer, context) - } -> std::same_as; - }; + { + T::postProcess(&composer, context) + } -> std::same_as; +}; /** * @brief This concept is used when we want to limit the number of executions of certain instructions (for example, @@ -183,9 +182,9 @@ concept PostProcessingEnabled = requires(Composer composer, Context context) { */ template concept InstructionWeightsEnabled = requires { - typename T::InstructionWeights; - T::InstructionWeights::_LIMIT; - }; + typename T::InstructionWeights; + T::InstructionWeights::_LIMIT; +}; /** * @brief Mutate the value of a field element diff --git a/barretenberg/cpp/src/barretenberg/relations/relation_types.hpp b/barretenberg/cpp/src/barretenberg/relations/relation_types.hpp index be4b87322548..1c4c69077ed7 100644 --- a/barretenberg/cpp/src/barretenberg/relations/relation_types.hpp +++ b/barretenberg/cpp/src/barretenberg/relations/relation_types.hpp @@ -23,10 +23,10 @@ using GetParameterView = std::conditional_t, template concept HasSubrelationLinearlyIndependentMember = requires(T) { - { - std::get(T::SUBRELATION_LINEARLY_INDEPENDENT) - } -> std::convertible_to; - }; + { + std::get(T::SUBRELATION_LINEARLY_INDEPENDENT) + } -> std::convertible_to; +}; template concept HasParameterLengthAdjustmentsMember = requires { T::TOTAL_LENGTH_ADJUSTMENTS; }; diff --git a/barretenberg/cpp/src/barretenberg/relations/utils.hpp b/barretenberg/cpp/src/barretenberg/relations/utils.hpp index 331967359f3e..63c8dad303e7 100644 --- a/barretenberg/cpp/src/barretenberg/relations/utils.hpp +++ b/barretenberg/cpp/src/barretenberg/relations/utils.hpp @@ -113,8 +113,7 @@ template class RelationUtils { template static constexpr void add_tuples(std::tuple& tuple_1, const std::tuple& tuple_2) { - auto add_tuples_helper = [&](std::index_sequence) - { + auto add_tuples_helper = [&](std::index_sequence) { ((std::get(tuple_1) += std::get(tuple_2)), ...); }; diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/circuit_builders/circuit_builders.hpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/circuit_builders/circuit_builders.hpp index 050fe337f8f2..6444cbaa8145 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/circuit_builders/circuit_builders.hpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/circuit_builders/circuit_builders.hpp @@ -13,5 +13,4 @@ concept HasPlookup = bb::IsAnyOf concept IsGoblinBuilder = bb::IsAnyOf; template -concept IsNotGoblinBuilder = ! -IsGoblinBuilder; +concept IsNotGoblinBuilder = !IsGoblinBuilder; diff --git a/barretenberg/cpp/src/barretenberg/transcript/transcript.hpp b/barretenberg/cpp/src/barretenberg/transcript/transcript.hpp index cf791175c98e..a396eb774cce 100644 --- a/barretenberg/cpp/src/barretenberg/transcript/transcript.hpp +++ b/barretenberg/cpp/src/barretenberg/transcript/transcript.hpp @@ -10,9 +10,9 @@ namespace bb::honk { template -concept Loggable = (std::same_as || std::same_as || - std::same_as || std::same_as || - std::same_as); +concept Loggable = + (std::same_as || std::same_as || std::same_as || + std::same_as || std::same_as); // class TranscriptManifest; class TranscriptManifest { diff --git a/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts b/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts index 402bfe97a096..dfbe9f843560 100644 --- a/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts @@ -172,6 +172,7 @@ export class Oracle { sortOrder: ACVMField[], [limit]: ACVMField[], [offset]: ACVMField[], + [status]: ACVMField[], [returnSize]: ACVMField[], ): Promise { const noteDatas = await this.typedOracle.getNotes( @@ -184,6 +185,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 4fd25983f9b4..69012b21bd55 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, @@ -142,6 +143,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 78dcc68d1397..fad43f0f2496 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 { BlockHeader, CallContext, @@ -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 7090aa52300f..f54221ea884d 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 { BlockHeader, CompleteAddress } 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 3d2e6d7d16eb..8527006662f9 100644 --- a/yarn-project/acir-simulator/src/client/view_data_oracle.ts +++ b/yarn-project/acir-simulator/src/client/view_data_oracle.ts @@ -4,6 +4,7 @@ import { CompleteAddress, INITIAL_L2_BLOCK_NUM, MerkleTreeId, + NoteStatus, NullifierMembershipWitness, PublicDataWitness, } from '@aztec/circuit-types'; @@ -203,6 +204,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( @@ -215,8 +217,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 0a5420ff7ba9..441f77f0d1f9 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 78fbeffd5535..7a1d6d32cbc7 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 4208: 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 34c7271bfebe..4b1c8949f3c0 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 4176e647578e..c4f2940f3a21 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 362526d13ffa..352d80bf1a38 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 4208: 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_get_notes.test.ts b/yarn-project/end-to-end/src/e2e_get_notes.test.ts new file mode 100644 index 000000000000..9ec0e6fca602 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_get_notes.test.ts @@ -0,0 +1,119 @@ +import { AztecAddress, Wallet, toBigInt } from '@aztec/aztec.js'; +import { TestContract } from '@aztec/noir-contracts'; + +import { setup } from './fixtures/utils.js'; + +describe('e2e_get_notes', () => { + let wallet: Wallet; + let teardown: () => Promise; + + let contract: TestContract; + let owner: AztecAddress; + + beforeAll(async () => { + ({ teardown, wallet } = await setup()); + + contract = await TestContract.deploy(wallet).send().deployed(); + owner = wallet.getCompleteAddress().address; + }, 100_000); + + afterAll(() => teardown()); + + 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 80a59556c0a6..7ab24aa181e4 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 6402ec444e81..13e9a4f23264 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,87 @@ 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 +324,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 fd32c595c34d..c5216b591de6 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, BlockHeader, CompleteAddress } from '@aztec/circuits.js'; import { ContractArtifact } from '@aztec/foundation/abi'; import { Fr, Point } from '@aztec/foundation/fields'; @@ -195,27 +195,45 @@ export class KVPxeDatabase implements PxeDatabase { } } + *#getAllNonNullifiedNoteIndices(): IterableIterator { + for (const [index, _] of this.#notes.entries()) { + if (this.#nullifiedNotes.has(index)) { + continue; + } + + yield index; + } + } + async getNotes(filter: NoteFilter): Promise { const publicKey: PublicKey | undefined = filter.owner ? (await 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()) - : undefined; + let candidateNoteIds: Array = []; + + filter.status = filter.status ?? NoteStatus.ACTIVE; + + if (filter.status == NoteStatus.ACTIVE || filter.status == NoteStatus.ACTIVE_OR_NULLIFIED) { + const candidateActiveNoteIds = 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.#getAllNonNullifiedNoteIndices(); + + candidateNoteIds = candidateNoteIds.concat([...candidateActiveNoteIds]); + } - if (!initialNoteIds) { - return Array.from(this.#getAllNonNullifiedNotes()); + if (filter.status == NoteStatus.ACTIVE_OR_NULLIFIED) { + candidateNoteIds = candidateNoteIds.concat([...this.#nullifiedNotes.keys()]); } const result: NoteDao[] = []; - for (const noteId of initialNoteIds) { + for (const noteId of candidateNoteIds) { const serializedNote = this.#notes.at(noteId); if (!serializedNote) { continue; 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 5f0a7ebccbf5..158ebb8f7d4c 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, MerkleTreeId, NoteFilter, randomTxHash } from '@aztec/circuit-types'; +import { INITIAL_L2_BLOCK_NUM, MerkleTreeId, NoteFilter, NoteStatus, randomTxHash } from '@aztec/circuit-types'; import { AztecAddress, BlockHeader, CompleteAddress } from '@aztec/circuits.js'; import { Fr, Point } from '@aztec/foundation/fields'; import { BenchmarkingContractArtifact } from '@aztec/noir-contracts/Benchmarking'; @@ -134,21 +134,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 50d62d00996f..04e9c0e8c72e 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,