diff --git a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr index f6182d59f83d..fc7722521040 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr @@ -66,3 +66,11 @@ pub unconstrained fn get_tx_effect(tx_hash: Field) -> Option { #[oracle(aztec_utl_getTxEffect)] unconstrained fn get_tx_effect_oracle(tx_hash: Field) -> Option {} + +/// Fetches all effects of settled transactions by hash, preserving request order. +pub unconstrained fn get_tx_effects(tx_hashes: EphemeralArray) -> EphemeralArray> { + get_tx_effects_oracle(tx_hashes) +} + +#[oracle(aztec_utl_getTxEffects)] +unconstrained fn get_tx_effects_oracle(tx_hashes: EphemeralArray) -> EphemeralArray> {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/version.nr b/noir-projects/aztec-nr/aztec/src/oracle/version.nr index cb77267bd8c5..6137d3d641d7 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 = 7; +pub global ORACLE_VERSION_MINOR: Field = 8; /// 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 18bf3305e570..09c53de0730b 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 @@ -273,6 +273,11 @@ export const ORACLE_REGISTRY = { returnType: OPTION(TX_EFFECT), }), + aztec_utl_getTxEffects: makeEntry({ + params: [{ name: 'txHashes', type: EPHEMERAL_ARRAY(TX_HASH) }], + returnType: EPHEMERAL_ARRAY(OPTION(TX_EFFECT)), + }), + aztec_utl_setCapsule: makeEntry({ params: [ { name: 'contractAddress', type: AZTEC_ADDRESS }, 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 3f7e6838ef02..995de85f15f1 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 @@ -34,6 +34,7 @@ import { BlockHeader, CallContext, Capsule, + DroppedTxReceipt, GlobalVariables, MinedTxReceipt, TxEffect, @@ -670,14 +671,19 @@ describe('Utility Execution test suite', () => { }); describe('node read cache', () => { - const makeMinedReceipt = (txHash: TxHash, txEffect = TxEffect.empty()) => + const makeTxEffect = (txHash: TxHash) => TxEffect.from({ ...TxEffect.empty(), txHash }); + const makeMinedReceipt = ( + txHash: TxHash, + txEffect = makeTxEffect(txHash), + blockNumber = BlockNumber(syncedBlockNumber), + ) => new MinedTxReceipt( txHash, TxStatus.FINALIZED, TxExecutionResult.SUCCESS, 0n, BlockHash.random(), - BlockNumber(syncedBlockNumber), + blockNumber, SlotNumber(1), 0, EpochNumber(1), @@ -727,6 +733,49 @@ describe('Utility Execution test suite', () => { expect(aztecNode.getTxReceipt).toHaveBeenCalledTimes(2); }); + it('returns aligned tx-effect options for tx hash batches', async () => { + const service = new EphemeralArrayService(); + const oracle = makeOracle({ scopes: [scope] }); + const presentTxHash = TxHash.random(); + const pendingTxHash = TxHash.random(); + const futureTxHash = TxHash.random(); + + aztecNode.getTxReceipt.mockImplementation(txHash => { + if (txHash.equals(presentTxHash)) { + return Promise.resolve(makeMinedReceipt(txHash)); + } + if (txHash.equals(futureTxHash)) { + return Promise.resolve(makeMinedReceipt(txHash, makeTxEffect(txHash), BlockNumber(syncedBlockNumber + 1))); + } + return Promise.resolve(new DroppedTxReceipt(txHash)); + }); + + const result = await oracle.getTxEffects( + EphemeralArray.fromValues(service, [presentTxHash, pendingTxHash, futureTxHash, presentTxHash]), + ); + const options = result.readAll(service); + + expect(options.map(option => option.isSome())).toEqual([true, false, false, true]); + expect(options[0].value?.txHash).toEqual(presentTxHash); + expect(options[3].value?.txHash).toEqual(presentTxHash); + expect(aztecNode.getTxReceipt).toHaveBeenCalledTimes(3); + }); + + it('does not share batched tx-effect reads across utility executions', async () => { + const service = new EphemeralArrayService(); + const txHash = TxHash.random(); + aztecNode.getTxReceipt.mockResolvedValue(makeMinedReceipt(txHash)); + + const firstOracle = makeOracle({ scopes: [scope] }); + const secondOracle = makeOracle({ scopes: [scope] }); + + await firstOracle.getTxEffects(EphemeralArray.fromValues(service, [txHash, txHash])); + expect(aztecNode.getTxReceipt).toHaveBeenCalledTimes(1); + + await secondOracle.getTxEffects(EphemeralArray.fromValues(service, [txHash])); + expect(aztecNode.getTxReceipt).toHaveBeenCalledTimes(2); + }); + it('reuses archive witness reads within a utility execution', async () => { const oracle = makeOracle({ scopes: [scope] }); const referenceBlockHash = 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 983df2003f32..125650e48871 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 @@ -35,6 +35,7 @@ import { type Capsule, type IndexedTxEffect, type OffchainEffect, + type TxEffect, type TxHash, } from '@aztec/stdlib/tx'; @@ -683,21 +684,25 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra throw new Error('Invalid tx hash passed into aztec_utl_getTxEffect oracle handler'); } - const receipt = await this.aztecNodeReadCache.getTxReceiptWithEffect(txHash); - if (!receipt.isMined() || !receipt.txEffect || receipt.blockNumber > this.anchorBlockHeader.getBlockNumber()) { - return Option.none(); + return await this.#getTxEffectOption(txHash); + } + + /** Fetches transaction effects for all hashes, preserving request order. */ + public async getTxEffects(txHashes: EphemeralArray): Promise>> { + const hashes = txHashes.readAll(this.ephemeralArrayService); + const invalidHash = hashes.find(txHash => txHash.hash.isZero()); + if (invalidHash) { + throw new Error('Invalid tx hash passed into aztec_utl_getTxEffects oracle handler'); } - const txEffect = receipt.txEffect; - return Option.some({ - ...txEffect, - publicLogs: FlatPublicLogs.fromLogs(txEffect.publicLogs), - contractClassLogs: txEffect.contractClassLogs.map(log => ({ - contractAddress: log.contractAddress, - fields: log.fields.toFields(), - emittedLength: log.emittedLength, - })), - }); + const uniqueTxHashes = uniqueBy(hashes, h => h.toString()); + const options = await Promise.all(uniqueTxHashes.map(txHash => this.#getTxEffectOption(txHash))); + const optionsByHash = new Map(uniqueTxHashes.map((txHash, i) => [txHash.toString(), options[i]])); + + return EphemeralArray.fromValues( + this.ephemeralArrayService, + hashes.map(txHash => optionsByHash.get(txHash.toString())!), + ); } public setCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[], scope: AztecAddress): void { @@ -1119,6 +1124,26 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra ); } + async #getTxEffectOption(txHash: TxHash): Promise> { + const receipt = await this.aztecNodeReadCache.getTxReceiptWithEffect(txHash); + if (!receipt.isMined() || !receipt.txEffect || receipt.blockNumber > this.anchorBlockHeader.getBlockNumber()) { + return Option.none(); + } + return Option.some(this.#toTxEffectData(receipt.txEffect)); + } + + #toTxEffectData(txEffect: TxEffect): TxEffectData { + return { + ...txEffect, + publicLogs: FlatPublicLogs.fromLogs(txEffect.publicLogs), + contractClassLogs: txEffect.contractClassLogs.map(log => ({ + contractAddress: log.contractAddress, + fields: log.fields.toFields(), + emittedLength: log.emittedLength, + })), + }; + } + /** Runs a query concurrently with a validation that the block hash is not ahead of the anchor block. */ async #queryWithBlockHashNotAfterAnchor(blockHash: BlockHash, query: () => Promise): Promise { // Most contracts query state at the "current" block, which is the anchor. Skip the validation when we can. diff --git a/yarn-project/pxe/src/oracle_version.ts b/yarn-project/pxe/src/oracle_version.ts index 688b384f4d14..92cf5a9d3aa1 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 = 7; +export const ORACLE_VERSION_MINOR = 8; /// 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 = 7; /// - 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 = '416b0e7d3e6dca5803ebe2a65ea93f1e79759dc39ef8ec4c52eeba5e2b0f3fca'; +export const ORACLE_INTERFACE_HASH = 'b302a8e65d37043c0a0dc08a39895603122dce334843f0442462edb3f56e32e2'; diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index 4190dc9c65d4..33c70f4c09f7 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -633,6 +633,15 @@ export class RPCTranslator { }); } + // eslint-disable-next-line camelcase + aztec_utl_getTxEffects(...inputs: ForeignCallArgs) { + return callTxeHandler({ + oracle: 'aztec_utl_getTxEffects', + inputs, + handler: ([txHashes]) => this.handlerAsUtility().getTxEffects(txHashes), + }); + } + // eslint-disable-next-line camelcase aztec_utl_setCapsule(...inputs: ForeignCallArgs) { return callTxeHandler({