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
6 changes: 6 additions & 0 deletions docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### [PXE] Local PXE database is reset on upgrade

The persisted tagging stores now key every entry by the self-describing `<kind>:<secret>:<app>` form of `AppTaggingSecret`; unconstrained secrets previously used a two-part `<secret>:<app>` key. This bumps the PXE data schema version, and there is no forward migration for the old keys: on first open the PXE clears any database whose stored schema version differs from the current one. The wipe resets the entire PXE store, not just the tagging data, because all of it shares one backing database.

**Impact**: On upgrade your local PXE state is reset. You must re-register accounts and re-sync from genesis. Wallets should surface a "your local state was reset, please re-register accounts and re-sync" path.

### [Aztec.nr] `TestEnvironmentOptions::with_tagging_secret_strategy` replaced

`TestEnvironmentOptions::with_tagging_secret_strategy` is now `with_default_tag_secret_strategy_all_modes` for tests
Expand Down
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -17,6 +20,12 @@ unconstrained fn get_block_hash_membership_witness_oracle(
block_hash: Field,
) -> Option<MembershipWitness<ARCHIVE_HEIGHT>> {}

#[oracle(aztec_utl_areBlockHashesInArchive)]
unconstrained fn are_block_hashes_in_archive_oracle(
anchor_block_hash: Field,
block_hashes: EphemeralArray<Field>,
) -> EphemeralArray<bool> {}

// 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
Expand Down Expand Up @@ -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<Field>,
) -> EphemeralArray<bool> {
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;
Expand Down
7 changes: 6 additions & 1 deletion noir-projects/aztec-nr/aztec/src/oracle/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
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 = 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() {
Expand Down
24 changes: 23 additions & 1 deletion yarn-project/aztec.js/src/utils/node.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BlockNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
import { TimeoutError } from '@aztec/foundation/error';
import { BlockHash } from '@aztec/stdlib/block';
import type { AztecNode } from '@aztec/stdlib/interfaces/client';
import {
Expand All @@ -14,7 +15,7 @@ import {

import { type MockProxy, mock } from 'jest-mock-extended';

import { waitForTx } from './node.js';
import { waitForNode, waitForTx } from './node.js';

describe('waitForTx', () => {
let node: MockProxy<AztecNode>;
Expand Down Expand Up @@ -137,3 +138,24 @@ describe('waitForTx', () => {
});
});
});

describe('waitForNode', () => {
let node: MockProxy<AztecNode>;

beforeEach(() => {
node = mock();
});

it('resolves once the node becomes reachable', async () => {
node.getNodeInfo.mockRejectedValueOnce(new Error('not up yet')).mockResolvedValueOnce({} as any);

await expect(waitForNode(node, undefined, { timeout: 5, interval: 0.01 })).resolves.toBeUndefined();
expect(node.getNodeInfo).toHaveBeenCalledTimes(2);
});

it('rejects with a TimeoutError when the node stays unreachable', async () => {
node.getNodeInfo.mockRejectedValue(new Error('unreachable'));

await expect(waitForNode(node, undefined, { timeout: 0.05, interval: 0.01 })).rejects.toThrow(TimeoutError);
});
});
37 changes: 25 additions & 12 deletions yarn-project/aztec.js/src/utils/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,31 @@ import { SortedTxStatuses, TxStatus } from '@aztec/stdlib/tx';

import { DefaultWaitOpts, type WaitOpts } from '../contract/wait_opts.js';

export const waitForNode = async (node: AztecNode, logger?: Logger) => {
await retryUntil(async () => {
try {
logger?.verbose('Attempting to contact Aztec node...');
await node.getNodeInfo();
logger?.verbose('Contacted Aztec node');
return true;
} catch {
logger?.verbose('Failed to contact Aztec Node');
}
return undefined;
}, 'RPC Get Node Info');
/**
* Waits for an Aztec node to become reachable, polling {@link AztecNode.getNodeInfo} until it succeeds.
* @param node - The Aztec node to contact.
* @param logger - Optional logger for polling progress.
* @param opts - Optional timeout and interval (in seconds). `timeout` defaults to {@link DefaultWaitOpts.timeout};
* pass `timeout: 0` to wait indefinitely.
* @throws TimeoutError if the node stays unreachable past the timeout.
*/
export const waitForNode = async (node: AztecNode, logger?: Logger, opts?: Pick<WaitOpts, 'timeout' | 'interval'>) => {
await retryUntil(
async () => {
try {
logger?.verbose('Attempting to contact Aztec node...');
await node.getNodeInfo();
logger?.verbose('Contacted Aztec node');
return true;
} catch {
logger?.verbose('Failed to contact Aztec Node');
}
return undefined;
},
'RPC Get Node Info',
opts?.timeout ?? DefaultWaitOpts.timeout,
opts?.interval ?? DefaultWaitOpts.interval,
);
};

/** Returns true if the receipt status is at least the desired status level. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { ARCHIVE_HEIGHT } from '@aztec/constants';
import { BlockNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
import { Fr } from '@aztec/foundation/curves/bn254';
import { promiseWithResolvers } from '@aztec/foundation/promise';
import { MembershipWitness } from '@aztec/foundation/trees';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import { BlockHash } from '@aztec/stdlib/block';
import type { AztecNode } from '@aztec/stdlib/interfaces/server';
import { PublicDataWitness } from '@aztec/stdlib/trees';
import { MinedTxReceipt, TxEffect, TxExecutionResult, TxHash, TxStatus } from '@aztec/stdlib/tx';

import { mock } from 'jest-mock-extended';

import { AztecNodeReadCache } from './aztec_node_read_cache.js';

describe('AztecNodeReadCache', () => {
let aztecNode: ReturnType<typeof mock<AztecNode>>;
let cache: AztecNodeReadCache;

const makeMinedReceipt = (txHash: TxHash) =>
new MinedTxReceipt(
txHash,
TxStatus.FINALIZED,
TxExecutionResult.SUCCESS,
0n,
BlockHash.random(),
BlockNumber(1),
SlotNumber(1),
0,
EpochNumber(1),
TxEffect.empty(),
);

beforeEach(() => {
aztecNode = mock<AztecNode>();
cache = new AztecNodeReadCache(aztecNode);
});

it('shares concurrent tx receipt reads', async () => {
const txHash = TxHash.random();
const deferred = promiseWithResolvers<MinedTxReceipt<{ includeTxEffect: true }>>();
aztecNode.getTxReceipt.mockReturnValue(deferred.promise);

const first = cache.getTxReceiptWithEffect(txHash);
const second = cache.getTxReceiptWithEffect(txHash);
const receipt = makeMinedReceipt(txHash);
deferred.resolve(receipt);

await expect(Promise.all([first, second])).resolves.toEqual([receipt, receipt]);
expect(aztecNode.getTxReceipt).toHaveBeenCalledTimes(1);
});

it('evicts rejected reads so callers can retry', async () => {
const txHash = TxHash.random();
const receipt = makeMinedReceipt(txHash);
aztecNode.getTxReceipt.mockRejectedValueOnce(new Error('temporary failure'));
aztecNode.getTxReceipt.mockResolvedValueOnce(receipt);

await expect(cache.getTxReceiptWithEffect(txHash)).rejects.toThrow('temporary failure');
await expect(cache.getTxReceiptWithEffect(txHash)).resolves.toBe(receipt);
expect(aztecNode.getTxReceipt).toHaveBeenCalledTimes(2);
});

it('keeps cache entries separate by method and arguments', async () => {
const blockHash = BlockHash.random();
const leafSlot = Fr.random();
const blockWitness = MembershipWitness.empty(ARCHIVE_HEIGHT);
const publicDataWitness = PublicDataWitness.random();
aztecNode.getBlockHashMembershipWitness.mockResolvedValue(blockWitness);
aztecNode.getPublicDataWitness.mockResolvedValue(publicDataWitness);

await expect(cache.getBlockHashMembershipWitness(blockHash, blockHash)).resolves.toBe(blockWitness);
await expect(cache.getPublicDataWitness(blockHash, leafSlot)).resolves.toBe(publicDataWitness);

expect(aztecNode.getBlockHashMembershipWitness).toHaveBeenCalledTimes(1);
expect(aztecNode.getPublicDataWitness).toHaveBeenCalledTimes(1);
});

it('caches successful undefined results', async () => {
const referenceBlockHash = BlockHash.random();
const blockHash = BlockHash.random();
aztecNode.getBlockHashMembershipWitness.mockResolvedValue(undefined);

await expect(cache.getBlockHashMembershipWitness(referenceBlockHash, blockHash)).resolves.toBeUndefined();
await expect(cache.getBlockHashMembershipWitness(referenceBlockHash, blockHash)).resolves.toBeUndefined();

expect(aztecNode.getBlockHashMembershipWitness).toHaveBeenCalledTimes(1);
});

it('reuses cached slots across overlapping public storage ranges', async () => {
const blockHash = BlockHash.random();
const contractAddress = await AztecAddress.random();
const startStorageSlot = new Fr(100);
aztecNode.getPublicStorageAt.mockImplementation((_block, _contract, slot) =>
Promise.resolve(new Fr(slot.value + 1n)),
);

await expect(cache.getPublicStorageRange(blockHash, contractAddress, startStorageSlot, 2)).resolves.toEqual([
new Fr(101),
new Fr(102),
]);
await expect(cache.getPublicStorageRange(blockHash, contractAddress, new Fr(101), 2)).resolves.toEqual([
new Fr(102),
new Fr(103),
]);

expect(aztecNode.getPublicStorageAt).toHaveBeenCalledTimes(3);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Fr } from '@aztec/foundation/curves/bn254';
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
import type { BlockHash, BlockParameter } from '@aztec/stdlib/block';
import type { AztecNode } from '@aztec/stdlib/interfaces/server';
import type { TxHash } from '@aztec/stdlib/tx';

/**
* Per-execution cache for immutable Aztec node reads.
*/
export class AztecNodeReadCache {
private readonly cache = new Map<string, Promise<unknown>>();

constructor(private readonly aztecNode: AztecNode) {}

/** Fetches a block without reissuing the same node request. */
public getBlock(block: BlockParameter) {
return this.#cachedRead(`block:${this.#keyPart(block)}`, () => this.aztecNode.getBlock(block));
}

/** Fetches a transaction receipt with its effect attached. */
public getTxReceiptWithEffect(txHash: TxHash) {
return this.#cachedRead(`tx-receipt-with-effect:${txHash.toString()}`, () =>
this.aztecNode.getTxReceipt(txHash, { includeTxEffect: true } as const),
);
}

/** Fetches an archive-tree witness for a block hash. */
public getBlockHashMembershipWitness(referenceBlock: BlockParameter, blockHash: BlockHash) {
return this.#cachedRead(
`block-hash-membership-witness:${this.#keyPart(referenceBlock)}:${blockHash.toString()}`,
() => this.aztecNode.getBlockHashMembershipWitness(referenceBlock, blockHash),
);
}

/** Fetches a public-data-tree witness for a leaf slot. */
public getPublicDataWitness(referenceBlock: BlockParameter, leafSlot: Fr) {
return this.#cachedRead(`public-data-witness:${this.#keyPart(referenceBlock)}:${leafSlot.toString()}`, () =>
this.aztecNode.getPublicDataWitness(referenceBlock, leafSlot),
);
}

/** Fetches public storage for a single slot. */
public getPublicStorageAt(referenceBlock: BlockParameter, contractAddress: AztecAddress, storageSlot: Fr) {
return this.#cachedRead(
`public-storage:${this.#keyPart(referenceBlock)}:${contractAddress.toString()}:${storageSlot.toString()}`,
() => this.aztecNode.getPublicStorageAt(referenceBlock, contractAddress, storageSlot),
);
}

/** Fetches a contiguous public storage range, reusing cached reads for overlapping slots. */
public getPublicStorageRange(
referenceBlock: BlockParameter,
contractAddress: AztecAddress,
startStorageSlot: Fr,
numberOfElements: number,
) {
const slots = Array(numberOfElements)
.fill(0)
.map((_, i) => new Fr(startStorageSlot.value + BigInt(i)));

return Promise.all(slots.map(storageSlot => this.getPublicStorageAt(referenceBlock, contractAddress, storageSlot)));
}

#cachedRead<T>(key: string, fetch: () => Promise<T>): Promise<T> {
const cached = this.cache.get(key);
if (cached) {
return cached as Promise<T>;
}

const promise = fetch();
promise.catch(() => this.cache.delete(key));
this.cache.set(key, promise);
return promise;
}

#keyPart(value: unknown): string {
if (['string', 'number', 'bigint', 'boolean'].includes(typeof value)) {
return String(value);
}
if (value && typeof value === 'object') {
const toString = (value as { toString?: () => string }).toString;
if (toString && toString !== Object.prototype.toString) {
return toString.call(value);
}
return JSON.stringify(value, (_key, nested) => (typeof nested === 'bigint' ? nested.toString() : nested));
}
return String(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
Loading
Loading