Skip to content

Commit 2c4d593

Browse files
authored
feat: merge-train/fairies-v5 (#24632)
BEGIN_COMMIT_OVERRIDE fix(aztec.js): give waitForNode a bounded default timeout (#24627) refactor: cache Aztec node reads per execution (#24630) feat(pxe)!: Add AppTaggingSecret kinds to keys in tagging stores (#24604) feat: add batch is block in archive oracle (#24634) END_COMMIT_OVERRIDE
2 parents 0302b85 + 2967179 commit 2c4d593

21 files changed

Lines changed: 585 additions & 102 deletions

File tree

docs/docs-developers/docs/resources/migration_notes.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ Aztec is in active development. Each version may introduce breaking changes that
99

1010
## TBD
1111

12+
### [PXE] Local PXE database is reset on upgrade
13+
14+
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.
15+
16+
**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.
17+
1218
### [Aztec.nr] `TestEnvironmentOptions::with_tagging_secret_strategy` replaced
1319

1420
`TestEnvironmentOptions::with_tagging_secret_strategy` is now `with_default_tag_secret_strategy_all_modes` for tests

noir-projects/aztec-nr/aztec/src/oracle/get_membership_witness.nr

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
use crate::protocol::{
2-
abis::block_header::BlockHeader,
3-
constants::{ARCHIVE_HEIGHT, NOTE_HASH_TREE_HEIGHT},
4-
merkle_tree::MembershipWitness,
5-
traits::Hash,
1+
use crate::{
2+
ephemeral::EphemeralArray,
3+
protocol::{
4+
abis::block_header::BlockHeader,
5+
constants::{ARCHIVE_HEIGHT, NOTE_HASH_TREE_HEIGHT},
6+
merkle_tree::MembershipWitness,
7+
traits::Hash,
8+
},
69
};
710

811
#[oracle(aztec_utl_getNoteHashMembershipWitness)]
@@ -17,6 +20,12 @@ unconstrained fn get_block_hash_membership_witness_oracle(
1720
block_hash: Field,
1821
) -> Option<MembershipWitness<ARCHIVE_HEIGHT>> {}
1922

23+
#[oracle(aztec_utl_areBlockHashesInArchive)]
24+
unconstrained fn are_block_hashes_in_archive_oracle(
25+
anchor_block_hash: Field,
26+
block_hashes: EphemeralArray<Field>,
27+
) -> EphemeralArray<bool> {}
28+
2029
// Note: get_nullifier_membership_witness function is implemented in get_nullifier_membership_witness.nr
2130

2231
/// 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(
5362
get_block_hash_membership_witness_oracle(anchor_block_hash, block_hash)
5463
}
5564

65+
/// Returns whether each block hash is present in the archive tree whose root is defined in `anchor_block_header`.
66+
pub unconstrained fn are_block_hashes_in_archive(
67+
anchor_block_header: BlockHeader,
68+
block_hashes: EphemeralArray<Field>,
69+
) -> EphemeralArray<bool> {
70+
let anchor_block_hash = anchor_block_header.hash();
71+
are_block_hashes_in_archive_oracle(anchor_block_hash, block_hashes)
72+
}
73+
5674
mod test {
5775
use crate::history::test::{create_note, NOTE_CREATED_AT};
5876
use crate::note::note_interface::NoteHash;

noir-projects/aztec-nr/aztec/src/oracle/mod.nr

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,12 @@ pub mod get_nullifier_membership_witness;
9999
],
100100
)]
101101
pub mod get_public_data_witness;
102-
#[generate_oracle_tests]
102+
#[generate_oracle_tests_excluding(
103+
@[
104+
// TODO: cover once the test resolver can serve EphemeralArray contents.
105+
quote { are_block_hashes_in_archive_oracle },
106+
],
107+
)]
103108
pub mod get_membership_witness;
104109
#[generate_oracle_tests]
105110
pub mod keys;

noir-projects/aztec-nr/aztec/src/oracle/version.nr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
/// immediately if AZTEC_NR_MINOR > PXE_MINOR because if a contract is updated to use a newer Aztec.nr dependency
1212
/// without actually using any of the new oracles then there is no reason to throw.
1313
pub global ORACLE_VERSION_MAJOR: Field = 30;
14-
pub global ORACLE_VERSION_MINOR: Field = 6;
14+
pub global ORACLE_VERSION_MINOR: Field = 7;
1515

1616
/// Asserts that the version of the oracle is compatible with the version expected by the contract.
1717
pub fn assert_compatible_oracle_version() {

yarn-project/aztec.js/src/utils/node.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { BlockNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
2+
import { TimeoutError } from '@aztec/foundation/error';
23
import { BlockHash } from '@aztec/stdlib/block';
34
import type { AztecNode } from '@aztec/stdlib/interfaces/client';
45
import {
@@ -14,7 +15,7 @@ import {
1415

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

17-
import { waitForTx } from './node.js';
18+
import { waitForNode, waitForTx } from './node.js';
1819

1920
describe('waitForTx', () => {
2021
let node: MockProxy<AztecNode>;
@@ -137,3 +138,24 @@ describe('waitForTx', () => {
137138
});
138139
});
139140
});
141+
142+
describe('waitForNode', () => {
143+
let node: MockProxy<AztecNode>;
144+
145+
beforeEach(() => {
146+
node = mock();
147+
});
148+
149+
it('resolves once the node becomes reachable', async () => {
150+
node.getNodeInfo.mockRejectedValueOnce(new Error('not up yet')).mockResolvedValueOnce({} as any);
151+
152+
await expect(waitForNode(node, undefined, { timeout: 5, interval: 0.01 })).resolves.toBeUndefined();
153+
expect(node.getNodeInfo).toHaveBeenCalledTimes(2);
154+
});
155+
156+
it('rejects with a TimeoutError when the node stays unreachable', async () => {
157+
node.getNodeInfo.mockRejectedValue(new Error('unreachable'));
158+
159+
await expect(waitForNode(node, undefined, { timeout: 0.05, interval: 0.01 })).rejects.toThrow(TimeoutError);
160+
});
161+
});

yarn-project/aztec.js/src/utils/node.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,31 @@ import { SortedTxStatuses, TxStatus } from '@aztec/stdlib/tx';
66

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

9-
export const waitForNode = async (node: AztecNode, logger?: Logger) => {
10-
await retryUntil(async () => {
11-
try {
12-
logger?.verbose('Attempting to contact Aztec node...');
13-
await node.getNodeInfo();
14-
logger?.verbose('Contacted Aztec node');
15-
return true;
16-
} catch {
17-
logger?.verbose('Failed to contact Aztec Node');
18-
}
19-
return undefined;
20-
}, 'RPC Get Node Info');
9+
/**
10+
* Waits for an Aztec node to become reachable, polling {@link AztecNode.getNodeInfo} until it succeeds.
11+
* @param node - The Aztec node to contact.
12+
* @param logger - Optional logger for polling progress.
13+
* @param opts - Optional timeout and interval (in seconds). `timeout` defaults to {@link DefaultWaitOpts.timeout};
14+
* pass `timeout: 0` to wait indefinitely.
15+
* @throws TimeoutError if the node stays unreachable past the timeout.
16+
*/
17+
export const waitForNode = async (node: AztecNode, logger?: Logger, opts?: Pick<WaitOpts, 'timeout' | 'interval'>) => {
18+
await retryUntil(
19+
async () => {
20+
try {
21+
logger?.verbose('Attempting to contact Aztec node...');
22+
await node.getNodeInfo();
23+
logger?.verbose('Contacted Aztec node');
24+
return true;
25+
} catch {
26+
logger?.verbose('Failed to contact Aztec Node');
27+
}
28+
return undefined;
29+
},
30+
'RPC Get Node Info',
31+
opts?.timeout ?? DefaultWaitOpts.timeout,
32+
opts?.interval ?? DefaultWaitOpts.interval,
33+
);
2134
};
2235

2336
/** Returns true if the receipt status is at least the desired status level. */
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { ARCHIVE_HEIGHT } from '@aztec/constants';
2+
import { BlockNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
3+
import { Fr } from '@aztec/foundation/curves/bn254';
4+
import { promiseWithResolvers } from '@aztec/foundation/promise';
5+
import { MembershipWitness } from '@aztec/foundation/trees';
6+
import { AztecAddress } from '@aztec/stdlib/aztec-address';
7+
import { BlockHash } from '@aztec/stdlib/block';
8+
import type { AztecNode } from '@aztec/stdlib/interfaces/server';
9+
import { PublicDataWitness } from '@aztec/stdlib/trees';
10+
import { MinedTxReceipt, TxEffect, TxExecutionResult, TxHash, TxStatus } from '@aztec/stdlib/tx';
11+
12+
import { mock } from 'jest-mock-extended';
13+
14+
import { AztecNodeReadCache } from './aztec_node_read_cache.js';
15+
16+
describe('AztecNodeReadCache', () => {
17+
let aztecNode: ReturnType<typeof mock<AztecNode>>;
18+
let cache: AztecNodeReadCache;
19+
20+
const makeMinedReceipt = (txHash: TxHash) =>
21+
new MinedTxReceipt(
22+
txHash,
23+
TxStatus.FINALIZED,
24+
TxExecutionResult.SUCCESS,
25+
0n,
26+
BlockHash.random(),
27+
BlockNumber(1),
28+
SlotNumber(1),
29+
0,
30+
EpochNumber(1),
31+
TxEffect.empty(),
32+
);
33+
34+
beforeEach(() => {
35+
aztecNode = mock<AztecNode>();
36+
cache = new AztecNodeReadCache(aztecNode);
37+
});
38+
39+
it('shares concurrent tx receipt reads', async () => {
40+
const txHash = TxHash.random();
41+
const deferred = promiseWithResolvers<MinedTxReceipt<{ includeTxEffect: true }>>();
42+
aztecNode.getTxReceipt.mockReturnValue(deferred.promise);
43+
44+
const first = cache.getTxReceiptWithEffect(txHash);
45+
const second = cache.getTxReceiptWithEffect(txHash);
46+
const receipt = makeMinedReceipt(txHash);
47+
deferred.resolve(receipt);
48+
49+
await expect(Promise.all([first, second])).resolves.toEqual([receipt, receipt]);
50+
expect(aztecNode.getTxReceipt).toHaveBeenCalledTimes(1);
51+
});
52+
53+
it('evicts rejected reads so callers can retry', async () => {
54+
const txHash = TxHash.random();
55+
const receipt = makeMinedReceipt(txHash);
56+
aztecNode.getTxReceipt.mockRejectedValueOnce(new Error('temporary failure'));
57+
aztecNode.getTxReceipt.mockResolvedValueOnce(receipt);
58+
59+
await expect(cache.getTxReceiptWithEffect(txHash)).rejects.toThrow('temporary failure');
60+
await expect(cache.getTxReceiptWithEffect(txHash)).resolves.toBe(receipt);
61+
expect(aztecNode.getTxReceipt).toHaveBeenCalledTimes(2);
62+
});
63+
64+
it('keeps cache entries separate by method and arguments', async () => {
65+
const blockHash = BlockHash.random();
66+
const leafSlot = Fr.random();
67+
const blockWitness = MembershipWitness.empty(ARCHIVE_HEIGHT);
68+
const publicDataWitness = PublicDataWitness.random();
69+
aztecNode.getBlockHashMembershipWitness.mockResolvedValue(blockWitness);
70+
aztecNode.getPublicDataWitness.mockResolvedValue(publicDataWitness);
71+
72+
await expect(cache.getBlockHashMembershipWitness(blockHash, blockHash)).resolves.toBe(blockWitness);
73+
await expect(cache.getPublicDataWitness(blockHash, leafSlot)).resolves.toBe(publicDataWitness);
74+
75+
expect(aztecNode.getBlockHashMembershipWitness).toHaveBeenCalledTimes(1);
76+
expect(aztecNode.getPublicDataWitness).toHaveBeenCalledTimes(1);
77+
});
78+
79+
it('caches successful undefined results', async () => {
80+
const referenceBlockHash = BlockHash.random();
81+
const blockHash = BlockHash.random();
82+
aztecNode.getBlockHashMembershipWitness.mockResolvedValue(undefined);
83+
84+
await expect(cache.getBlockHashMembershipWitness(referenceBlockHash, blockHash)).resolves.toBeUndefined();
85+
await expect(cache.getBlockHashMembershipWitness(referenceBlockHash, blockHash)).resolves.toBeUndefined();
86+
87+
expect(aztecNode.getBlockHashMembershipWitness).toHaveBeenCalledTimes(1);
88+
});
89+
90+
it('reuses cached slots across overlapping public storage ranges', async () => {
91+
const blockHash = BlockHash.random();
92+
const contractAddress = await AztecAddress.random();
93+
const startStorageSlot = new Fr(100);
94+
aztecNode.getPublicStorageAt.mockImplementation((_block, _contract, slot) =>
95+
Promise.resolve(new Fr(slot.value + 1n)),
96+
);
97+
98+
await expect(cache.getPublicStorageRange(blockHash, contractAddress, startStorageSlot, 2)).resolves.toEqual([
99+
new Fr(101),
100+
new Fr(102),
101+
]);
102+
await expect(cache.getPublicStorageRange(blockHash, contractAddress, new Fr(101), 2)).resolves.toEqual([
103+
new Fr(102),
104+
new Fr(103),
105+
]);
106+
107+
expect(aztecNode.getPublicStorageAt).toHaveBeenCalledTimes(3);
108+
});
109+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Fr } from '@aztec/foundation/curves/bn254';
2+
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
3+
import type { BlockHash, BlockParameter } from '@aztec/stdlib/block';
4+
import type { AztecNode } from '@aztec/stdlib/interfaces/server';
5+
import type { TxHash } from '@aztec/stdlib/tx';
6+
7+
/**
8+
* Per-execution cache for immutable Aztec node reads.
9+
*/
10+
export class AztecNodeReadCache {
11+
private readonly cache = new Map<string, Promise<unknown>>();
12+
13+
constructor(private readonly aztecNode: AztecNode) {}
14+
15+
/** Fetches a block without reissuing the same node request. */
16+
public getBlock(block: BlockParameter) {
17+
return this.#cachedRead(`block:${this.#keyPart(block)}`, () => this.aztecNode.getBlock(block));
18+
}
19+
20+
/** Fetches a transaction receipt with its effect attached. */
21+
public getTxReceiptWithEffect(txHash: TxHash) {
22+
return this.#cachedRead(`tx-receipt-with-effect:${txHash.toString()}`, () =>
23+
this.aztecNode.getTxReceipt(txHash, { includeTxEffect: true } as const),
24+
);
25+
}
26+
27+
/** Fetches an archive-tree witness for a block hash. */
28+
public getBlockHashMembershipWitness(referenceBlock: BlockParameter, blockHash: BlockHash) {
29+
return this.#cachedRead(
30+
`block-hash-membership-witness:${this.#keyPart(referenceBlock)}:${blockHash.toString()}`,
31+
() => this.aztecNode.getBlockHashMembershipWitness(referenceBlock, blockHash),
32+
);
33+
}
34+
35+
/** Fetches a public-data-tree witness for a leaf slot. */
36+
public getPublicDataWitness(referenceBlock: BlockParameter, leafSlot: Fr) {
37+
return this.#cachedRead(`public-data-witness:${this.#keyPart(referenceBlock)}:${leafSlot.toString()}`, () =>
38+
this.aztecNode.getPublicDataWitness(referenceBlock, leafSlot),
39+
);
40+
}
41+
42+
/** Fetches public storage for a single slot. */
43+
public getPublicStorageAt(referenceBlock: BlockParameter, contractAddress: AztecAddress, storageSlot: Fr) {
44+
return this.#cachedRead(
45+
`public-storage:${this.#keyPart(referenceBlock)}:${contractAddress.toString()}:${storageSlot.toString()}`,
46+
() => this.aztecNode.getPublicStorageAt(referenceBlock, contractAddress, storageSlot),
47+
);
48+
}
49+
50+
/** Fetches a contiguous public storage range, reusing cached reads for overlapping slots. */
51+
public getPublicStorageRange(
52+
referenceBlock: BlockParameter,
53+
contractAddress: AztecAddress,
54+
startStorageSlot: Fr,
55+
numberOfElements: number,
56+
) {
57+
const slots = Array(numberOfElements)
58+
.fill(0)
59+
.map((_, i) => new Fr(startStorageSlot.value + BigInt(i)));
60+
61+
return Promise.all(slots.map(storageSlot => this.getPublicStorageAt(referenceBlock, contractAddress, storageSlot)));
62+
}
63+
64+
#cachedRead<T>(key: string, fetch: () => Promise<T>): Promise<T> {
65+
const cached = this.cache.get(key);
66+
if (cached) {
67+
return cached as Promise<T>;
68+
}
69+
70+
const promise = fetch();
71+
promise.catch(() => this.cache.delete(key));
72+
this.cache.set(key, promise);
73+
return promise;
74+
}
75+
76+
#keyPart(value: unknown): string {
77+
if (['string', 'number', 'bigint', 'boolean'].includes(typeof value)) {
78+
return String(value);
79+
}
80+
if (value && typeof value === 'object') {
81+
const toString = (value as { toString?: () => string }).toString;
82+
if (toString && toString !== Object.prototype.toString) {
83+
return toString.call(value);
84+
}
85+
return JSON.stringify(value, (_key, nested) => (typeof nested === 'bigint' ? nested.toString() : nested));
86+
}
87+
return String(value);
88+
}
89+
}

yarn-project/pxe/src/contract_function_simulator/oracle/oracle_registry.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ export const ORACLE_REGISTRY = {
141141
returnType: OPTION(MEMBERSHIP_WITNESS(ARCHIVE_HEIGHT)),
142142
}),
143143

144+
aztec_utl_areBlockHashesInArchive: makeEntry({
145+
params: [
146+
{ name: 'anchorBlockHash', type: BLOCK_HASH },
147+
{ name: 'blockHashes', type: EPHEMERAL_ARRAY(BLOCK_HASH) },
148+
],
149+
returnType: EPHEMERAL_ARRAY(BOOL),
150+
}),
151+
144152
aztec_utl_getNullifierMembershipWitness: makeEntry({
145153
params: [
146154
{ name: 'blockHash', type: BLOCK_HASH },

0 commit comments

Comments
 (0)