Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,11 @@ pub unconstrained fn get_tx_effect(tx_hash: Field) -> Option<TxEffect> {

#[oracle(aztec_utl_getTxEffect)]
unconstrained fn get_tx_effect_oracle(tx_hash: Field) -> Option<TxEffect> {}

/// Fetches all effects of settled transactions by hash, preserving request order.
pub unconstrained fn get_tx_effects(tx_hashes: EphemeralArray<Field>) -> EphemeralArray<Option<TxEffect>> {
get_tx_effects_oracle(tx_hashes)
}

#[oracle(aztec_utl_getTxEffects)]
unconstrained fn get_tx_effects_oracle(tx_hashes: EphemeralArray<Field>) -> EphemeralArray<Option<TxEffect>> {}
2 changes: 1 addition & 1 deletion noir-projects/aztec-nr/aztec/src/oracle/version.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
BlockHeader,
CallContext,
Capsule,
DroppedTxReceipt,
GlobalVariables,
MinedTxReceipt,
TxEffect,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
type Capsule,
type IndexedTxEffect,
type OffchainEffect,
type TxEffect,
type TxHash,
} from '@aztec/stdlib/tx';

Expand Down Expand Up @@ -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<TxHash>): Promise<EphemeralArray<Option<TxEffectData>>> {
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 {
Expand Down Expand Up @@ -1119,6 +1124,26 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra
);
}

async #getTxEffectOption(txHash: TxHash): Promise<Option<TxEffectData>> {
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<T>(blockHash: BlockHash, query: () => Promise<T>): Promise<T> {
// Most contracts query state at the "current" block, which is the anchor. Skip the validation when we can.
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/pxe/src/oracle_version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
/// 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:
/// - increment `ORACLE_VERSION_MAJOR` and reset `ORACLE_VERSION_MINOR` to zero if the change is breaking, or
/// - 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';
9 changes: 9 additions & 0 deletions yarn-project/txe/src/rpc_translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading