From 923962a49380ef5147e1fa6d255f54481d626c8d Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 9 Jul 2026 14:28:48 +0000 Subject: [PATCH 1/2] add batch is block in archive oracle --- .../src/oracle/get_membership_witness.nr | 28 +++++++++++++++---- .../aztec-nr/aztec/src/oracle/mod.nr | 7 ++++- .../aztec-nr/aztec/src/oracle/version.nr | 2 +- .../oracle/oracle_registry.ts | 8 ++++++ .../oracle/utility_execution.test.ts | 21 ++++++++++++++ .../oracle/utility_execution_oracle.ts | 16 +++++++++++ yarn-project/pxe/src/oracle_version.ts | 4 +-- yarn-project/txe/src/rpc_translator.ts | 10 +++++++ 8 files changed, 87 insertions(+), 9 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/oracle/get_membership_witness.nr b/noir-projects/aztec-nr/aztec/src/oracle/get_membership_witness.nr index 1b77cd7d7885..d81e99a71067 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/get_membership_witness.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/get_membership_witness.nr @@ -1,8 +1,11 @@ -use crate::protocol::{ - abis::block_header::BlockHeader, - constants::{ARCHIVE_HEIGHT, NOTE_HASH_TREE_HEIGHT}, - merkle_tree::MembershipWitness, - traits::Hash, +use crate::{ + ephemeral::EphemeralArray, + protocol::{ + abis::block_header::BlockHeader, + constants::{ARCHIVE_HEIGHT, NOTE_HASH_TREE_HEIGHT}, + merkle_tree::MembershipWitness, + traits::Hash, + }, }; #[oracle(aztec_utl_getNoteHashMembershipWitness)] @@ -17,6 +20,12 @@ unconstrained fn get_block_hash_membership_witness_oracle( block_hash: Field, ) -> Option> {} +#[oracle(aztec_utl_areBlockHashesInArchive)] +unconstrained fn are_block_hashes_in_archive_oracle( + anchor_block_hash: Field, + block_hashes: EphemeralArray, +) -> EphemeralArray {} + // Note: get_nullifier_membership_witness function is implemented in get_nullifier_membership_witness.nr /// Returns a membership witness for a `note_hash` in the note hash tree whose root is defined in @@ -53,6 +62,15 @@ pub unconstrained fn get_maybe_block_hash_membership_witness( get_block_hash_membership_witness_oracle(anchor_block_hash, block_hash) } +/// Returns whether each block hash is present in the archive tree whose root is defined in `anchor_block_header`. +pub unconstrained fn are_block_hashes_in_archive( + anchor_block_header: BlockHeader, + block_hashes: EphemeralArray, +) -> EphemeralArray { + let anchor_block_hash = anchor_block_header.hash(); + are_block_hashes_in_archive_oracle(anchor_block_hash, block_hashes) +} + mod test { use crate::history::test::{create_note, NOTE_CREATED_AT}; use crate::note::note_interface::NoteHash; diff --git a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr index 3fe750eb3e10..c84420c7fac0 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr @@ -99,7 +99,12 @@ pub mod get_nullifier_membership_witness; ], )] pub mod get_public_data_witness; -#[generate_oracle_tests] +#[generate_oracle_tests_excluding( + @[ + // TODO: cover once the test resolver can serve EphemeralArray contents. + quote { are_block_hashes_in_archive_oracle }, + ], +)] pub mod get_membership_witness; #[generate_oracle_tests] pub mod keys; diff --git a/noir-projects/aztec-nr/aztec/src/oracle/version.nr b/noir-projects/aztec-nr/aztec/src/oracle/version.nr index 54924c79d5d3..cb77267bd8c5 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/version.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/version.nr @@ -11,7 +11,7 @@ /// immediately if AZTEC_NR_MINOR > PXE_MINOR because if a contract is updated to use a newer Aztec.nr dependency /// without actually using any of the new oracles then there is no reason to throw. pub global ORACLE_VERSION_MAJOR: Field = 30; -pub global ORACLE_VERSION_MINOR: Field = 6; +pub global ORACLE_VERSION_MINOR: Field = 7; /// Asserts that the version of the oracle is compatible with the version expected by the contract. pub fn assert_compatible_oracle_version() { diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_registry.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_registry.ts index b9bba4a8efc5..18bf3305e570 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_registry.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_registry.ts @@ -141,6 +141,14 @@ export const ORACLE_REGISTRY = { returnType: OPTION(MEMBERSHIP_WITNESS(ARCHIVE_HEIGHT)), }), + aztec_utl_areBlockHashesInArchive: makeEntry({ + params: [ + { name: 'anchorBlockHash', type: BLOCK_HASH }, + { name: 'blockHashes', type: EPHEMERAL_ARRAY(BLOCK_HASH) }, + ], + returnType: EPHEMERAL_ARRAY(BOOL), + }), + aztec_utl_getNullifierMembershipWitness: makeEntry({ params: [ { name: 'blockHash', type: BLOCK_HASH }, 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 6e5c956302f8..3f7e6838ef02 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 @@ -741,6 +741,27 @@ describe('Utility Execution test suite', () => { expect(aztecNode.getBlockHashMembershipWitness).toHaveBeenCalledTimes(1); }); + it('returns aligned archive-membership booleans for block hash batches', async () => { + const service = new EphemeralArrayService(); + const oracle = makeOracle({ scopes: [scope] }); + const referenceBlockHash = await anchorBlockHeader.hash(); + const presentBlockHash = BlockHash.random(); + const missingBlockHash = BlockHash.random(); + const witness = MembershipWitness.empty(ARCHIVE_HEIGHT); + + aztecNode.getBlockHashMembershipWitness.mockImplementation((_referenceBlockHash, blockHash) => + Promise.resolve(blockHash.equals(presentBlockHash) ? witness : undefined), + ); + + const result = await oracle.areBlockHashesInArchive( + referenceBlockHash, + EphemeralArray.fromValues(service, [presentBlockHash, missingBlockHash, presentBlockHash]), + ); + + expect(result.readAll(service)).toEqual([true, false, true]); + expect(aztecNode.getBlockHashMembershipWitness).toHaveBeenCalledTimes(2); + }); + it('reuses public storage reads within a utility execution', async () => { const oracle = makeOracle({ scopes: [scope] }); const blockHash = await anchorBlockHeader.hash(); 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 4672d421cfc9..983df2003f32 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 @@ -273,6 +273,22 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra return witness ? Option.some(witness) : Option.none(); } + /** Returns whether each block hash is present in the archive tree at the referenced block. */ + public async areBlockHashesInArchive( + referenceBlockHash: BlockHash, + blockHashes: EphemeralArray, + ): Promise> { + const hashes = blockHashes.readAll(this.ephemeralArrayService); + const memberships = await this.#queryWithBlockHashNotAfterAnchor(referenceBlockHash, () => + Promise.all( + hashes.map(blockHash => + this.aztecNodeReadCache.getBlockHashMembershipWitness(referenceBlockHash, blockHash).then(Boolean), + ), + ), + ); + return EphemeralArray.fromValues(this.ephemeralArrayService, memberships); + } + /** * Returns a nullifier membership witness for a given nullifier at a given block. * @param blockHash - The block hash at which to get the index. diff --git a/yarn-project/pxe/src/oracle_version.ts b/yarn-project/pxe/src/oracle_version.ts index 2072d50d221d..688b384f4d14 100644 --- a/yarn-project/pxe/src/oracle_version.ts +++ b/yarn-project/pxe/src/oracle_version.ts @@ -11,7 +11,7 @@ /// if AZTEC_NR_MINOR > PXE_MINOR because if a contract is updated to use a newer Aztec.nr dependency without actually /// using any of the new oracles then there is no reason to throw. export const ORACLE_VERSION_MAJOR = 30; -export const ORACLE_VERSION_MINOR = 6; +export const ORACLE_VERSION_MINOR = 7; /// This hash is computed from the `ORACLE_REGISTRY` declaration (each oracle's name, ordered parameter names and /// types, and return type) and is used to detect when the oracle interface changes. When it does, you need to either: @@ -19,4 +19,4 @@ export const ORACLE_VERSION_MINOR = 6; /// - increment only `ORACLE_VERSION_MINOR` if the change is additive (a new oracle was added). /// /// These constants must be kept in sync between this file and `noir-projects/aztec-nr/aztec/src/oracle/version.nr`. -export const ORACLE_INTERFACE_HASH = '6094994f539407001d2adce5b6a8792c08ef645ee39813e32390f6208e78bc16'; +export const ORACLE_INTERFACE_HASH = '416b0e7d3e6dca5803ebe2a65ea93f1e79759dc39ef8ec4c52eeba5e2b0f3fca'; diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index 0a8cbb71759f..4190dc9c65d4 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -572,6 +572,16 @@ export class RPCTranslator { }); } + // eslint-disable-next-line camelcase + aztec_utl_areBlockHashesInArchive(...inputs: ForeignCallArgs) { + return callTxeHandler({ + oracle: 'aztec_utl_areBlockHashesInArchive', + inputs, + handler: ([anchorBlockHash, blockHashes]) => + this.handlerAsUtility().areBlockHashesInArchive(anchorBlockHash, blockHashes), + }); + } + // eslint-disable-next-line camelcase aztec_utl_getLowNullifierMembershipWitness(...inputs: ForeignCallArgs) { return callTxeHandler({ From 6b4e27d06d473cb2e15cc40bf9dd5251972205c6 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 9 Jul 2026 15:13:24 +0000 Subject: [PATCH 2/2] make it easier to re-use standard parts of the sync pipeline --- .../aztec-nr/aztec/src/macros/aztec.nr | 4 +- .../aztec/src/messages/discovery/mod.nr | 148 +++++++++++++++--- 2 files changed, 128 insertions(+), 24 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr index 350c05d6d263..b0c8c3c1db14 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr @@ -64,7 +64,9 @@ impl AztecConfig { /// The generated `sync_state` function will call `handler` instead of /// [`crate::messages::discovery::do_sync_state`]. The handler receives all of the same parameters, so it can /// run custom logic (e.g. fetching and decrypting custom logs) before, after, or instead of calling - /// `do_sync_state`. + /// `do_sync_state`. Handlers that want the standard discovery flow with custom logic before storage can call + /// [`crate::messages::discovery::discover_enqueued_notes_and_events`] and then + /// [`crate::messages::processing::validate_and_store_enqueued_notes_and_events`]. /// /// `handler` must be a function that conforms to the /// [`crate::messages::discovery::CustomSyncHandler`] type signature. diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr index 8f8de2f43aaa..514d833e1f11 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr @@ -115,7 +115,9 @@ pub type CustomMessageHandler = unconstrained fn( /// /// When set via [`crate::macros::AztecConfig::custom_sync_state`], the generated `sync_state` function will call /// this handler instead of [`do_sync_state`]. It receives all of the same parameters, so it can run custom logic -/// (e.g. fetching and decrypting custom logs) before, after, or instead of calling [`do_sync_state`]. +/// before, after, or instead of calling [`do_sync_state`]. Custom handlers that want the standard discovery pipeline +/// with a custom processing step before storage can call [`discover_enqueued_notes_and_events`] followed by +/// [`validate_and_store_enqueued_notes_and_events`]. pub type CustomSyncHandler = unconstrained fn( /* contract_address */AztecAddress, /* compute_note_hash */ ComputeNoteHash, @@ -140,16 +142,15 @@ pub unconstrained fn do_sync_state( offchain_inbox_sync: Option, scope: AztecAddress, ) { - let provided_secrets = get_handshake_secrets(contract_address, scope); - sync_state_with_secrets( + discover_enqueued_notes_and_events( contract_address, compute_note_hash, compute_note_nullifier, process_custom_message, offchain_inbox_sync, scope, - provided_secrets, ); + validate_and_store_enqueued_notes_and_events(scope); } /// Variant of [`do_sync_state`] that skips handshake secret discovery. Used by the HandshakeRegistry to avoid @@ -161,9 +162,54 @@ pub unconstrained fn do_sync_state_without_handshake_discovery( process_custom_message: Option, offchain_inbox_sync: Option, scope: AztecAddress, +) { + discover_enqueued_notes_and_events_without_handshake_discovery( + contract_address, + compute_note_hash, + compute_note_nullifier, + process_custom_message, + offchain_inbox_sync, + scope, + ); + validate_and_store_enqueued_notes_and_events(scope); +} + +/// Runs the standard recipient discovery pipeline and leaves discovered notes/events enqueued for later validation. +/// +/// This performs the discovery work used by [`do_sync_state`] but intentionally does not call +/// [`validate_and_store_enqueued_notes_and_events`]. Custom sync handlers can call this function, inspect or mutate the +/// enqueued requests, then explicitly call [`validate_and_store_enqueued_notes_and_events`] when ready. +pub unconstrained fn discover_enqueued_notes_and_events( + contract_address: AztecAddress, + compute_note_hash: ComputeNoteHash, + compute_note_nullifier: ComputeNoteNullifier, + process_custom_message: Option, + offchain_inbox_sync: Option, + scope: AztecAddress, +) { + let provided_secrets = get_handshake_secrets(contract_address, scope); + discover_enqueued_notes_and_events_with_secrets( + contract_address, + compute_note_hash, + compute_note_nullifier, + process_custom_message, + offchain_inbox_sync, + scope, + provided_secrets, + ); +} + +/// Variant of [`discover_enqueued_notes_and_events`] that skips handshake secret discovery. +pub unconstrained fn discover_enqueued_notes_and_events_without_handshake_discovery( + contract_address: AztecAddress, + compute_note_hash: ComputeNoteHash, + compute_note_nullifier: ComputeNoteNullifier, + process_custom_message: Option, + offchain_inbox_sync: Option, + scope: AztecAddress, ) { let provided_secrets = EphemeralArray::::empty_at(PROVIDED_SECRETS_ARRAY_BASE_SLOT); - sync_state_with_secrets( + discover_enqueued_notes_and_events_with_secrets( contract_address, compute_note_hash, compute_note_nullifier, @@ -174,7 +220,7 @@ pub unconstrained fn do_sync_state_without_handshake_discovery( ); } -unconstrained fn sync_state_with_secrets( +unconstrained fn discover_enqueued_notes_and_events_with_secrets( contract_address: AztecAddress, compute_note_hash: ComputeNoteHash, compute_note_nullifier: ComputeNoteNullifier, @@ -230,41 +276,33 @@ unconstrained fn sync_state_with_secrets( compute_note_nullifier, scope, ); - - // Finally we validate all notes and events that were found as part of the previous processes, resulting in them - // being added to PXE's database and retrievable via oracles (get_notes) and our TS API (PXE::getPrivateEvents). - validate_and_store_enqueued_notes_and_events(scope); } mod test { use crate::ephemeral::EphemeralArray; use crate::messages::{ - delivery::handshake::HandshakePage, - discovery::{CustomMessageHandler, do_sync_state}, + discovery::{ + CustomMessageHandler, discover_enqueued_notes_and_events_without_handshake_discovery, + do_sync_state_without_handshake_discovery, + }, logs::note::MAX_NOTE_PACKED_LEN, processing::{offchain::OffchainInboxSync, pending_tagged_log::PendingTaggedLog}, }; - use crate::protocol::{address::AztecAddress, traits::Serialize}; + use crate::protocol::address::AztecAddress; use crate::test::helpers::test_environment::TestEnvironment; use std::test::OracleMock; #[test] unconstrained fn do_sync_state_does_not_panic_on_empty_logs() { - let mut env = TestEnvironment::new(); - let scope = env.create_light_account(); - let contract_address = AztecAddress { inner: 0xdeadbeef }; + let scope = AztecAddress { inner: 0xfeed }; + let env = TestEnvironment::new(); env.utility_context_at(contract_address, |_| { - // get_handshake_secrets calls the HandshakeRegistry during sync; return an empty page so - // no tagging secrets are produced. - let empty_page = HandshakePage { items: BoundedVec::new(), total_count: 0 }; - let _ = OracleMock::mock("aztec_utl_callUtilityFunction").returns(empty_page.serialize()); - // Mock the oracle call to return a known base slot, then populate an ephemeral // array at that slot so do_sync_state processes a non-empty log list. let base_slot = 42; - let mock = std::test::OracleMock::mock("aztec_utl_getPendingTaggedLogsV2"); + let mock = OracleMock::mock("aztec_utl_getPendingTaggedLogsV2"); let _ = mock.returns(base_slot); let logs: EphemeralArray = EphemeralArray::at(base_slot); @@ -273,7 +311,7 @@ mod test { let no_handler: Option = Option::none(); let no_inbox_sync: Option = Option::none(); - do_sync_state( + do_sync_state_without_handshake_discovery( contract_address, dummy_compute_note_hash, dummy_compute_note_nullifier, @@ -288,6 +326,70 @@ mod test { }); } + #[test] + unconstrained fn discover_enqueued_notes_and_events_does_not_store_enqueued_requests() { + let contract_address = AztecAddress { inner: 0xdeadbeef }; + let scope = AztecAddress { inner: 0xfeed }; + + let env = TestEnvironment::new(); + env.utility_context_at(contract_address, |_| { + let logs_slot = 43; + let logs_mock = OracleMock::mock("aztec_utl_getPendingTaggedLogsV2"); + let _ = logs_mock.returns(logs_slot); + + let logs: EphemeralArray = EphemeralArray::at(logs_slot); + logs.push(PendingTaggedLog { log: BoundedVec::new(), context: std::mem::zeroed() }); + + let store_mock = OracleMock::mock("aztec_utl_validateAndStoreEnqueuedNotesAndEvents"); + + let no_handler: Option = Option::none(); + let no_inbox_sync: Option = Option::none(); + discover_enqueued_notes_and_events_without_handshake_discovery( + contract_address, + dummy_compute_note_hash, + dummy_compute_note_nullifier, + no_handler, + no_inbox_sync, + scope, + ); + + assert_eq(logs_mock.times_called(), 1); + assert_eq(store_mock.times_called(), 0); + }); + } + + #[test] + unconstrained fn do_sync_state_stores_enqueued_requests_after_discovery() { + let contract_address = AztecAddress { inner: 0xdeadbeef }; + let scope = AztecAddress { inner: 0xfeed }; + + let env = TestEnvironment::new(); + env.utility_context_at(contract_address, |_| { + let logs_slot = 44; + let logs_mock = OracleMock::mock("aztec_utl_getPendingTaggedLogsV2"); + let _ = logs_mock.returns(logs_slot); + + let logs: EphemeralArray = EphemeralArray::at(logs_slot); + logs.push(PendingTaggedLog { log: BoundedVec::new(), context: std::mem::zeroed() }); + + let store_mock = OracleMock::mock("aztec_utl_validateAndStoreEnqueuedNotesAndEvents"); + + let no_handler: Option = Option::none(); + let no_inbox_sync: Option = Option::none(); + do_sync_state_without_handshake_discovery( + contract_address, + dummy_compute_note_hash, + dummy_compute_note_nullifier, + no_handler, + no_inbox_sync, + scope, + ); + + assert_eq(logs_mock.times_called(), 1); + assert_eq(store_mock.times_called(), 1); + }); + } + unconstrained fn dummy_compute_note_hash( _packed_note: BoundedVec, _owner: AztecAddress,