diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr index 3f7fa63a0291..8b23babf1814 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr @@ -36,6 +36,10 @@ global EVENT_VALIDATION_REQUESTS_ARRAY_BASE_SLOT: Field = sha256_to_field( "AZTEC_NR::EVENT_VALIDATION_REQUESTS_ARRAY_BASE_SLOT".as_bytes(), ); +global NOTE_BOUNDED_VEC_CAPACITY_SLOT: Field = sha256_to_field("AZTEC_NR::NOTE_BOUNDED_VEC_CAPACITY_SLOT".as_bytes()); + +global EVENT_BOUNDED_VEC_CAPACITY_SLOT: Field = sha256_to_field("AZTEC_NR::EVENT_BOUNDED_VEC_CAPACITY_SLOT".as_bytes()); + global LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT: Field = sha256_to_field( "AZTEC_NR::LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT".as_bytes(), ); @@ -150,6 +154,19 @@ pub unconstrained fn enqueue_event_for_validation( /// /// This automatically clears both validation request queues, so no further work needs to be done by the caller. pub unconstrained fn validate_and_store_enqueued_notes_and_events(contract_address: AztecAddress) { + // Store BoundedVec capacities so PXE knows the serialization layout. Contracts that don't store these will + // cause PXE to fall back to default capacities. + oracle::capsules::store( + contract_address, + NOTE_BOUNDED_VEC_CAPACITY_SLOT, + [MAX_NOTE_PACKED_LEN as Field], + ); + oracle::capsules::store( + contract_address, + EVENT_BOUNDED_VEC_CAPACITY_SLOT, + [MAX_EVENT_SERIALIZED_LEN as Field], + ); + oracle::message_processing::validate_and_store_enqueued_notes_and_events( contract_address, NOTE_VALIDATION_REQUESTS_ARRAY_BASE_SLOT, diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.test.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.test.ts index 63816730790f..05eae4398fff 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.test.ts @@ -6,21 +6,23 @@ import { TxHash } from '@aztec/stdlib/tx'; import { EventValidationRequest } from './event_validation_request.js'; describe('EventValidationRequest', () => { - it('output of Noir serialization deserializes as expected', () => { + it('deserializes with default capacity when no capacity is given', () => { + // 11 storage fields = default BoundedVec capacity (default) const serialized = [ 1, // contract_address 2, // event_type_id 3, // randomness - 4, // serialized_event[0] - 5, // serialized_event[1] - 0, // serialized_event padding start + 4, + 5, 0, 0, 0, 0, 0, 0, - 0, // serialized_event padding end + 0, + 0, + 0, // 11 storage fields 2, // bounded_vec_len 6, // event_commitment 7, // tx_hash @@ -37,4 +39,59 @@ describe('EventValidationRequest', () => { expect(request.txHash).toEqual(TxHash.fromBigInt(7n)); expect(request.recipient).toEqual(AztecAddress.fromBigInt(8n)); }); + + it('deserializes with explicit capacity matching current capacity', () => { + // 10 storage fields = current BoundedVec capacity + const serialized = [ + 1, // contract_address + 2, // event_type_id + 3, // randomness + 4, + 5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, // 10 storage fields + 2, // bounded_vec_len + 6, // event_commitment + 7, // tx_hash + 8, // recipient + ].map(n => new Fr(n)); + + const request = EventValidationRequest.fromFields(serialized, 10); + + expect(request.contractAddress).toEqual(AztecAddress.fromBigInt(1n)); + expect(request.serializedEvent).toEqual([new Fr(4), new Fr(5)]); + expect(request.eventCommitment).toEqual(new Fr(6)); + }); + + it('throws if capacity does not match actual field count (reader not exhausted)', () => { + // Data has 11 storage fields but we claim capacity=10, leaving 1 unconsumed field + const serialized = [ + 1, + 2, + 3, // header + 10, + 11, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, // 11 storage fields + 2, // bounded_vec_len + 6, + 7, + 8, // footer + ].map(n => new Fr(n)); + + expect(() => EventValidationRequest.fromFields(serialized, 10)).toThrow(/did not consume all fields/); + }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.ts index 8a33dd551923..6cd0cc6ef187 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.ts @@ -4,8 +4,9 @@ import { EventSelector } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { TxHash } from '@aztec/stdlib/tx'; -// TODO(#14617): should we compute this from constants? This value is aztec-nr specific. -const MAX_EVENT_SERIALIZED_LEN = 10; +// Default BoundedVec storage capacity for contracts that don't explicitly store their capacity. +// TODO(F-380): remove once all contracts store capacity explicitly. +export const DEFAULT_EVENT_BOUNDED_VEC_CAPACITY = 11; /** * Intermediate struct used to perform batch event validation by PXE. The `utilityValidateAndStoreEnqueuedNotesAndEvents` oracle @@ -22,7 +23,7 @@ export class EventValidationRequest { public recipient: AztecAddress, ) {} - static fromFields(fields: Fr[] | FieldReader): EventValidationRequest { + static fromFields(fields: Fr[], capacity: number = DEFAULT_EVENT_BOUNDED_VEC_CAPACITY): EventValidationRequest { const reader = FieldReader.asReader(fields); const contractAddress = AztecAddress.fromField(reader.readField()); @@ -30,7 +31,7 @@ export class EventValidationRequest { const randomness = reader.readField(); - const eventStorage = reader.readFieldArray(MAX_EVENT_SERIALIZED_LEN); + const eventStorage = reader.readFieldArray(capacity); const eventLen = reader.readField().toNumber(); const serializedEvent = eventStorage.slice(0, eventLen); @@ -38,6 +39,12 @@ export class EventValidationRequest { const txHash = TxHash.fromField(reader.readField()); const recipient = AztecAddress.fromField(reader.readField()); + if (reader.remainingFields() !== 0) { + throw new Error( + `EventValidationRequest deserialization did not consume all fields: ${reader.remainingFields()} remaining (capacity=${capacity}).`, + ); + } + return new EventValidationRequest( contractAddress, eventTypeId, diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_response.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_response.ts index 020270e8821a..baa4b3c58e39 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_response.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_response.ts @@ -3,10 +3,7 @@ import { range } from '@aztec/foundation/array'; import { Fr } from '@aztec/foundation/curves/bn254'; import type { TxHash } from '@aztec/stdlib/tx'; -import { MAX_NOTE_PACKED_LEN } from './note_validation_request.js'; - -const MAX_PUBLIC_LOG_LEN_FOR_NOTE_COMPLETION = MAX_NOTE_PACKED_LEN; -const MAX_LOG_CONTENT_LEN = Math.max(MAX_PUBLIC_LOG_LEN_FOR_NOTE_COMPLETION, PRIVATE_LOG_CIPHERTEXT_LEN); +const MAX_LOG_CONTENT_LEN = PRIVATE_LOG_CIPHERTEXT_LEN; /** * Intermediate struct used to perform batch log retrieval by PXE. The `utilityBulkRetrieveLogs` oracle stores values of this diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.test.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.test.ts index 4ac64de1d016..dc673588b8d6 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.test.ts @@ -5,27 +5,29 @@ import { TxHash } from '@aztec/stdlib/tx'; import { NoteValidationRequest } from './note_validation_request.js'; describe('NoteValidationRequest', () => { - it('output of Noir serialization deserializes as expected', () => { + it('deserializes with default capacity when no capacity is given', () => { + // 9 storage fields = old BoundedVec capacity (default) const serialized = [ - '0x0000000000000000000000000000000000000000000000000000000000000001', // contract address - '0x0000000000000000000000000000000000000000000000000000000000000032', // owner - '0x0000000000000000000000000000000000000000000000000000000000000002', // storage slot - '0x000000000000000000000000000000000000000000000000000000000000002a', // randomness - '0x0000000000000000000000000000000000000000000000000000000000000003', // note nonce - '0x0000000000000000000000000000000000000000000000000000000000000004', // content begin: note content 1 - '0x0000000000000000000000000000000000000000000000000000000000000005', // note content 2 - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', // content end (MAX_NOTE_PACKED_LEN = 8) - '0x0000000000000000000000000000000000000000000000000000000000000002', // content length - '0x0000000000000000000000000000000000000000000000000000000000000006', // note hash - '0x0000000000000000000000000000000000000000000000000000000000000007', // nullifier - '0x0000000000000000000000000000000000000000000000000000000000000008', // tx hash - '0x0000000000000000000000000000000000000000000000000000000000000009', // recipient - ].map(Fr.fromHexString); + 1, // contract address + 50, // owner + 2, // storage slot + 42, // randomness + 3, // note nonce + 4, + 5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, // 9 storage fields + 2, // content length + 6, // note hash + 7, // nullifier + 8, // tx hash + 9, // recipient + ].map(n => new Fr(n)); const request = NoteValidationRequest.fromFields(serialized); @@ -41,32 +43,60 @@ describe('NoteValidationRequest', () => { expect(request.recipient).toEqual(AztecAddress.fromBigInt(9n)); }); - it('throws if fed more fields than expected', () => { + it('deserializes with explicit capacity matching current capacity', () => { + // 8 storage fields = current BoundedVec capacity const serialized = [ - '0x0000000000000000000000000000000000000000000000000000000000000001', // contract address - '0x0000000000000000000000000000000000000000000000000000000000000032', // owner - '0x0000000000000000000000000000000000000000000000000000000000000002', // storage slot - '0X000000000000000000000000000000000000000000000000000000000000002a', // randomness - '0x0000000000000000000000000000000000000000000000000000000000000003', // note nonce - '0x0000000000000000000000000000000000000000000000000000000000000004', // content begin: note content 1 - '0x0000000000000000000000000000000000000000000000000000000000000005', // note content 2 - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', // content end (MAX_NOTE_PACKED_LEN = 8) - '0x0000000000000000000000000000000000000000000000000000000000000000', // extra field beyond MAX_NOTE_PACKED_LEN, this is a malformed serialization - '0x0000000000000000000000000000000000000000000000000000000000000002', // content length - '0x0000000000000000000000000000000000000000000000000000000000000006', // note hash - '0x0000000000000000000000000000000000000000000000000000000000000007', // nullifier - '0x0000000000000000000000000000000000000000000000000000000000000008', // tx hash - '0x0000000000000000000000000000000000000000000000000000000000000009', // recipient - ].map(Fr.fromHexString); + 1, // contract address + 50, // owner + 2, // storage slot + 42, // randomness + 3, // note nonce + 4, + 5, + 0, + 0, + 0, + 0, + 0, + 0, // 8 storage fields + 2, // content length + 6, // note hash + 7, // nullifier + 8, // tx hash + 9, // recipient + ].map(n => new Fr(n)); - expect(() => NoteValidationRequest.fromFields(serialized)).toThrow( - /Error converting array of fields to NoteValidationRequest/, - ); + const request = NoteValidationRequest.fromFields(serialized, 8); + + expect(request.contractAddress).toEqual(AztecAddress.fromBigInt(1n)); + expect(request.content).toEqual([new Fr(4), new Fr(5)]); + expect(request.noteHash).toEqual(new Fr(6)); + }); + + it('throws if capacity does not match actual field count (reader not exhausted)', () => { + // Data has 9 storage fields but we claim capacity=8, leaving 1 unconsumed field + const serialized = [ + 1, + 2, + 3, + 4, + 5, // header + 10, + 11, + 0, + 0, + 0, + 0, + 0, + 0, + 0, // 9 storage fields + 2, // content length + 6, + 7, + 8, + 9, // footer + ].map(n => new Fr(n)); + + expect(() => NoteValidationRequest.fromFields(serialized, 8)).toThrow(/did not consume all fields/); }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.ts index 02ebba99e96e..b797e42d946e 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.ts @@ -3,8 +3,9 @@ import { FieldReader } from '@aztec/foundation/serialize'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { TxHash } from '@aztec/stdlib/tx'; -// TODO(#14617): should we compute this from constants? This value is aztec-nr specific. -export const MAX_NOTE_PACKED_LEN = 8; +// Default BoundedVec storage capacity for contracts that don't explicitly store their capacity. +// TODO(F-380): remove once all contracts store capacity explicitly. +export const DEFAULT_NOTE_BOUNDED_VEC_CAPACITY = 9; /** * Intermediate struct used to perform batch note validation by PXE. The `utilityValidateAndStoreEnqueuedNotesAndEvents` oracle @@ -24,7 +25,7 @@ export class NoteValidationRequest { public recipient: AztecAddress, ) {} - static fromFields(fields: Fr[] | FieldReader): NoteValidationRequest { + static fromFields(fields: Fr[], capacity: number = DEFAULT_NOTE_BOUNDED_VEC_CAPACITY): NoteValidationRequest { const reader = FieldReader.asReader(fields); const contractAddress = AztecAddress.fromField(reader.readField()); @@ -33,7 +34,7 @@ export class NoteValidationRequest { const randomness = reader.readField(); const noteNonce = reader.readField(); - const contentStorage = reader.readFieldArray(MAX_NOTE_PACKED_LEN); + const contentStorage = reader.readFieldArray(capacity); const contentLen = reader.readField().toNumber(); const content = contentStorage.slice(0, contentLen); @@ -44,7 +45,7 @@ export class NoteValidationRequest { if (reader.remainingFields() !== 0) { throw new Error( - `Error converting array of fields to NoteValidationRequest. Hint: check that MAX_NOTE_PACKED_LEN is consistent with private_notes::MAX_NOTE_PACKED_LEN in Aztec-nr.`, + `NoteValidationRequest deserialization did not consume all fields: ${reader.remainingFields()} remaining (capacity=${capacity}).`, ); } diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts index fd6cb7ef4a2a..7fb86e7a4939 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts @@ -4,15 +4,16 @@ import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import type { KeyStore } from '@aztec/key-store'; import { StatefulTestContractArtifact } from '@aztec/noir-test-contracts.js/StatefulTest'; import { WASMSimulator } from '@aztec/simulator/client'; -import { FunctionCall, FunctionSelector, FunctionType, encodeArguments } from '@aztec/stdlib/abi'; +import { EventSelector, FunctionCall, FunctionSelector, FunctionType, encodeArguments } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { BlockHash } from '@aztec/stdlib/block'; import { CompleteAddress, type ContractInstanceWithAddress } from '@aztec/stdlib/contract'; +import { computeUniqueNoteHash, siloNoteHash, siloNullifier } from '@aztec/stdlib/hash'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import { deriveKeys } from '@aztec/stdlib/keys'; import { Note, NoteDao } from '@aztec/stdlib/note'; import { makeL2Tips } from '@aztec/stdlib/testing'; -import { BlockHeader, GlobalVariables, TxHash } from '@aztec/stdlib/tx'; +import { BlockHeader, GlobalVariables, TxEffect, TxHash } from '@aztec/stdlib/tx'; import { mock } from 'jest-mock-extended'; import type { _MockProxy } from 'jest-mock-extended/lib/Mock.js'; @@ -27,7 +28,13 @@ import type { RecipientTaggingStore } from '../../storage/tagging_store/recipien import type { SenderAddressBookStore } from '../../storage/tagging_store/sender_address_book_store.js'; import type { SenderTaggingStore } from '../../storage/tagging_store/sender_tagging_store.js'; import { ContractFunctionSimulator } from '../contract_function_simulator.js'; -import { UtilityExecutionOracle } from './utility_execution_oracle.js'; +import { DEFAULT_EVENT_BOUNDED_VEC_CAPACITY } from '../noir-structs/event_validation_request.js'; +import { DEFAULT_NOTE_BOUNDED_VEC_CAPACITY } from '../noir-structs/note_validation_request.js'; +import { + EVENT_BOUNDED_VEC_CAPACITY_SLOT, + NOTE_BOUNDED_VEC_CAPACITY_SLOT, + UtilityExecutionOracle, +} from './utility_execution_oracle.js'; describe('Utility Execution test suite', () => { const simulator = new WASMSimulator(); @@ -231,5 +238,179 @@ describe('Utility Execution test suite', () => { ); }); }); + + describe('utilityValidateAndStoreEnqueuedNotesAndEvents', () => { + const noteSlot = new Fr(100); + const eventSlot = new Fr(200); + + const noteContractAddress = AztecAddress.fromField(new Fr(1)); + const noteHash = new Fr(6); + const noteNonce = new Fr(3); + const noteTxHash = TxHash.fromField(new Fr(8)); + + const eventContractAddress = AztecAddress.fromField(new Fr(10)); + const eventCommitment = new Fr(60); + const eventTxHash = TxHash.fromField(new Fr(80)); + + const noteContentValues = [4, 5]; + const eventContentValues = [40, 50]; + + it('deserializes and stores notes and events when capacity is explicit in capsule', async () => { + mockCapsuleStore({ noteCapacity: 8, eventCapacity: 8 }); + await mockNode(); + + await utilityExecutionOracle.utilityValidateAndStoreEnqueuedNotesAndEvents( + contractAddress, + noteSlot, + eventSlot, + ); + + assertNoteStoredCorrectly(); + assertEventStoredCorrectly(); + assertCapsulesCleanedUp(); + }); + + it('deserializes and stores notes and events using default capacity when capsule has none', async () => { + mockCapsuleStore(); + await mockNode(); + + await utilityExecutionOracle.utilityValidateAndStoreEnqueuedNotesAndEvents( + contractAddress, + noteSlot, + eventSlot, + ); + + assertNoteStoredCorrectly(); + assertEventStoredCorrectly(); + assertCapsulesCleanedUp(); + }); + + // --- Helpers --- + + // Wire format: contract_address, owner, storage_slot, randomness, nonce, + // ...noteContent (padded to capacity), contentLen, noteHash, nullifier, txHash, recipient + function buildSerializedNote(capacity: number): Fr[] { + const padding = new Array(capacity - noteContentValues.length).fill(0); + return [1, 50, 2, 42, 3, ...noteContentValues, ...padding, noteContentValues.length, 6, 7, 8, 9].map( + v => new Fr(v), + ); + } + + // Wire format: contract_address, event_type_id, randomness, + // ...eventContent (padded to capacity), contentLen, event_commitment, tx_hash, recipient + function buildSerializedEvent(capacity: number): Fr[] { + const padding = new Array(capacity - eventContentValues.length).fill(0); + return [10, 20, 30, ...eventContentValues, ...padding, eventContentValues.length, 60, 80, 90].map( + v => new Fr(v), + ); + } + + /** Populates the capsule store with one serialized note and one event request, using explicit or default capacity. */ + function mockCapsuleStore(opts: { noteCapacity?: number; eventCapacity?: number } = {}) { + const noteSerializedCapacity = opts.noteCapacity ?? DEFAULT_NOTE_BOUNDED_VEC_CAPACITY; + const eventSerializedCapacity = opts.eventCapacity ?? DEFAULT_EVENT_BOUNDED_VEC_CAPACITY; + + const capsules = new Map(); + if (opts.noteCapacity !== undefined) { + capsules.set(`${contractAddress.toString()}:${NOTE_BOUNDED_VEC_CAPACITY_SLOT.toString()}`, [ + new Fr(opts.noteCapacity), + ]); + } + if (opts.eventCapacity !== undefined) { + capsules.set(`${contractAddress.toString()}:${EVENT_BOUNDED_VEC_CAPACITY_SLOT.toString()}`, [ + new Fr(opts.eventCapacity), + ]); + } + + const capsuleArrays = new Map(); + capsuleArrays.set(`${contractAddress.toString()}:${noteSlot.toString()}`, [ + buildSerializedNote(noteSerializedCapacity), + ]); + capsuleArrays.set(`${contractAddress.toString()}:${eventSlot.toString()}`, [ + buildSerializedEvent(eventSerializedCapacity), + ]); + + capsuleStore.loadCapsule.mockImplementation((address, slot) => + Promise.resolve(capsules.get(`${address.toString()}:${slot.toString()}`) ?? null), + ); + capsuleStore.readCapsuleArray.mockImplementation((address, slot) => + Promise.resolve(capsuleArrays.get(`${address.toString()}:${slot.toString()}`) ?? []), + ); + capsuleStore.setCapsuleArray.mockImplementation((address, slot, content) => { + capsuleArrays.set(`${address.toString()}:${slot.toString()}`, content); + return Promise.resolve(); + }); + capsuleStore.deleteCapsule.mockImplementation(() => {}); + } + + /** Mocks aztecNode to return the expected TxEffect (with note hashes or nullifiers) based on tx hash. */ + async function mockNode() { + const uniqueNoteHash = await computeUniqueNoteHash( + noteNonce, + await siloNoteHash(noteContractAddress, noteHash), + ); + const noteTxEffect = TxEffect.empty(); + noteTxEffect.txHash = noteTxHash; + noteTxEffect.noteHashes = [uniqueNoteHash]; + + const siloedEventCommitment = await siloNullifier(eventContractAddress, eventCommitment); + const eventTxEffect = TxEffect.empty(); + eventTxEffect.txHash = eventTxHash; + eventTxEffect.nullifiers = [siloedEventCommitment]; + + const blockHash = BlockHash.random(); + aztecNode.getTxEffect.mockImplementation((txHash: TxHash) => { + const data = txHash.equals(noteTxHash) ? noteTxEffect : eventTxEffect; + return Promise.resolve({ + l2BlockNumber: BlockNumber(syncedBlockNumber - 1), + l2BlockHash: blockHash, + data, + txIndexInBlock: 0, + }); + }); + aztecNode.findLeavesIndexes.mockResolvedValue([undefined]); + } + + function assertNoteStoredCorrectly() { + expect(noteStore.addNotes).toHaveBeenCalledTimes(1); + const storedNotes: NoteDao[] = noteStore.addNotes.mock.calls[0][0]; + expect(storedNotes).toHaveLength(1); + const noteDao = storedNotes[0]; + expect(noteDao.note).toEqual(new Note([new Fr(4), new Fr(5)])); + expect(noteDao.contractAddress).toEqual(noteContractAddress); + expect(noteDao.owner).toEqual(AztecAddress.fromField(new Fr(50))); + expect(noteDao.storageSlot).toEqual(new Fr(2)); + expect(noteDao.randomness).toEqual(new Fr(42)); + expect(noteDao.noteNonce).toEqual(noteNonce); + expect(noteDao.noteHash).toEqual(noteHash); + expect(noteDao.txHash).toEqual(noteTxHash); + } + + function assertEventStoredCorrectly() { + expect(privateEventStore.storePrivateEventLog).toHaveBeenCalledTimes(1); + const call = privateEventStore.storePrivateEventLog.mock.calls[0]; + expect(call[0]).toEqual(EventSelector.fromField(new Fr(20))); + expect(call[1]).toEqual(new Fr(30)); + expect(call[2]).toEqual([new Fr(40), new Fr(50)]); + expect(call[4].contractAddress).toEqual(eventContractAddress); + expect(call[4].txHash).toEqual(eventTxHash); + expect(call[4].scope).toEqual(AztecAddress.fromField(new Fr(90))); + } + + function assertCapsulesCleanedUp() { + expect(capsuleStore.deleteCapsule).toHaveBeenCalledWith( + contractAddress, + NOTE_BOUNDED_VEC_CAPACITY_SLOT, + 'test-job-id', + ); + expect(capsuleStore.deleteCapsule).toHaveBeenCalledWith( + contractAddress, + EVENT_BOUNDED_VEC_CAPACITY_SLOT, + 'test-job-id', + ); + expect(capsuleStore.setCapsuleArray).toHaveBeenCalledWith(contractAddress, noteSlot, [], 'test-job-id'); + expect(capsuleStore.setCapsuleArray).toHaveBeenCalledWith(contractAddress, eventSlot, [], 'test-job-id'); + } + }); }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index f957d44326a8..05d54f5d3e0f 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -1,6 +1,7 @@ import type { ARCHIVE_HEIGHT, NOTE_HASH_TREE_HEIGHT } from '@aztec/constants'; import type { BlockNumber } from '@aztec/foundation/branded-types'; import { Aes128 } from '@aztec/foundation/crypto/aes128'; +import { sha256ToField } from '@aztec/foundation/crypto/sha256'; import { Fr } from '@aztec/foundation/curves/bn254'; import { Point } from '@aztec/foundation/curves/grumpkin'; import { LogLevels, type Logger, createLogger } from '@aztec/foundation/log'; @@ -42,6 +43,12 @@ import { pickNotes } from '../pick_notes.js'; import type { IMiscOracle, IUtilityExecutionOracle, NoteData } from './interfaces.js'; import { MessageLoadOracleInputs } from './message_load_oracle_inputs.js'; +// Capsule slots where aztec-nr stores BoundedVec capacities (must match Noir globals in messages/processing/mod.nr). +export const NOTE_BOUNDED_VEC_CAPACITY_SLOT = sha256ToField([Buffer.from('AZTEC_NR::NOTE_BOUNDED_VEC_CAPACITY_SLOT')]); +export const EVENT_BOUNDED_VEC_CAPACITY_SLOT = sha256ToField([ + Buffer.from('AZTEC_NR::EVENT_BOUNDED_VEC_CAPACITY_SLOT'), +]); + /** Args for UtilityExecutionOracle constructor. */ export type UtilityExecutionOracleArgs = { contractAddress: AztecAddress; @@ -458,15 +465,32 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra throw new Error(`Got a note validation request from ${contractAddress}, expected ${this.contractAddress}`); } + // Read BoundedVec capacities from the capsule store. Contracts that don't store these explicitly will + // have null here, so fromFields will fall back to default capacities. + // TODO(F-380): make capacity required once all contracts store it explicitly. + const noteCapacityCapsule = await this.capsuleStore.loadCapsule( + contractAddress, + NOTE_BOUNDED_VEC_CAPACITY_SLOT, + this.jobId, + ); + const noteCapacity = noteCapacityCapsule?.[0]?.toNumber(); + + const eventCapacityCapsule = await this.capsuleStore.loadCapsule( + contractAddress, + EVENT_BOUNDED_VEC_CAPACITY_SLOT, + this.jobId, + ); + const eventCapacity = eventCapacityCapsule?.[0]?.toNumber(); + // We read all note and event validation requests and process them all concurrently. This makes the process much // faster as we don't need to wait for the network round-trip. const noteValidationRequests = ( await this.capsuleStore.readCapsuleArray(contractAddress, noteValidationRequestsArrayBaseSlot, this.jobId) - ).map(NoteValidationRequest.fromFields); + ).map(fields => NoteValidationRequest.fromFields(fields, noteCapacity)); const eventValidationRequests = ( await this.capsuleStore.readCapsuleArray(contractAddress, eventValidationRequestsArrayBaseSlot, this.jobId) - ).map(EventValidationRequest.fromFields); + ).map(fields => EventValidationRequest.fromFields(fields, eventCapacity)); const noteService = new NoteService(this.noteStore, this.aztecNode, this.anchorBlockHeader, this.jobId); const noteStorePromises = noteValidationRequests.map(request => @@ -500,6 +524,8 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra await Promise.all([...noteStorePromises, ...eventStorePromises]); // Requests are cleared once we're done. + this.capsuleStore.deleteCapsule(contractAddress, NOTE_BOUNDED_VEC_CAPACITY_SLOT, this.jobId); + this.capsuleStore.deleteCapsule(contractAddress, EVENT_BOUNDED_VEC_CAPACITY_SLOT, this.jobId); await this.capsuleStore.setCapsuleArray(contractAddress, noteValidationRequestsArrayBaseSlot, [], this.jobId); await this.capsuleStore.setCapsuleArray(contractAddress, eventValidationRequestsArrayBaseSlot, [], this.jobId); }