Skip to content

Commit cf188b6

Browse files
committed
fix(pxe): retry settled-read witness fetches and self-diagnose misses in kernel reset
A settled note-hash/nullifier read request whose membership witness the node cannot serve at the tx anchor block currently fails the whole proving job on the first miss. The anchor block is data the node has already synced, so a miss is either a transient node-side visibility race or a genuinely absent leaf. Retry the hint-building batch a bounded number of times for the former, and enrich the final error for the latter with the anchor block number/hash plus a latest-view insertion probe that tells a stale anchor apart from node inconsistency and from a bogus read request.
1 parent 59d45f6 commit cf188b6

3 files changed

Lines changed: 190 additions & 19 deletions

File tree

yarn-project/pxe/src/private_kernel/hints/private_kernel_reset_private_inputs_builder.test.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import {
88
NOTE_HASH_TREE_HEIGHT,
99
VK_TREE_HEIGHT,
1010
} from '@aztec/constants';
11+
import { BlockNumber } from '@aztec/foundation/branded-types';
1112
import { Fr } from '@aztec/foundation/curves/bn254';
1213
import type { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin';
1314
import { MembershipWitness } from '@aztec/foundation/trees';
1415
import { AztecAddress } from '@aztec/stdlib/aztec-address';
16+
import { BlockHash } from '@aztec/stdlib/block';
1517
import {
1618
type DimensionName,
1719
type PrivateKernelResetDimensions,
@@ -146,15 +148,37 @@ describe('PrivateKernelResetPrivateInputsBuilder', () => {
146148
expect(oracle.getNoteHashMembershipWitness).toHaveBeenCalledTimes(numSettled);
147149
});
148150

151+
it('retries and recovers when a settled read witness is transiently missing', async () => {
152+
kernel.addSettledNoteHashReadRequest();
153+
154+
oracle.getNoteHashMembershipWitness.mockResolvedValueOnce(undefined);
155+
156+
const builder = makeResetBuilder();
157+
expect(builder.needsReset()).toBe(true);
158+
159+
const result = await builder.build(oracle);
160+
expectDimensions(builder, result.dimensions, { NOTE_HASH_SETTLED_READ: 1 });
161+
expect(oracle.getNoteHashMembershipWitness).toHaveBeenCalledTimes(2);
162+
});
163+
149164
it('throws when settled read request has no matching membership witness', async () => {
150165
kernel.addSettledNoteHashReadRequest();
151166

152167
oracle.getNoteHashMembershipWitness.mockResolvedValue(undefined);
168+
oracle.getAnchorBlockNumber.mockReturnValue(BlockNumber(42));
169+
oracle.getAnchorBlockHash.mockResolvedValue(BlockHash.random());
170+
oracle.findLeafInsertionBlock.mockResolvedValue(BlockNumber(45));
153171

154172
const builder = makeResetBuilder();
155173
expect(builder.needsReset()).toBe(true);
156174

157-
await expect(builder.build(oracle)).rejects.toThrow('Read request is reading an unknown note hash.');
175+
const err = await builder.build(oracle).then(
176+
() => undefined,
177+
(e: Error) => e,
178+
);
179+
expect(err?.message).toContain('Read request is reading an unknown note hash');
180+
expect(err?.message).toContain('Anchor block 42');
181+
expect(err?.message).toContain('latest view: leaf inserted in block 45');
158182
});
159183

160184
it('throws when pending read request has no matching note hash', () => {
@@ -212,15 +236,37 @@ describe('PrivateKernelResetPrivateInputsBuilder', () => {
212236
expect(oracle.getNullifierMembershipWitness).toHaveBeenCalledTimes(numSettled);
213237
});
214238

239+
it('retries and recovers when a settled read witness is transiently missing', async () => {
240+
kernel.addSettledNullifierReadRequest();
241+
242+
oracle.getNullifierMembershipWitness.mockResolvedValueOnce(undefined);
243+
244+
const builder = makeResetBuilder();
245+
expect(builder.needsReset()).toBe(true);
246+
247+
const result = await builder.build(oracle);
248+
expectDimensions(builder, result.dimensions, { NULLIFIER_SETTLED_READ: 1 });
249+
expect(oracle.getNullifierMembershipWitness).toHaveBeenCalledTimes(2);
250+
});
251+
215252
it('throws when settled read request has no matching membership witness', async () => {
216253
kernel.addSettledNullifierReadRequest();
217254

218255
oracle.getNullifierMembershipWitness.mockResolvedValue(undefined);
256+
oracle.getAnchorBlockNumber.mockReturnValue(BlockNumber(42));
257+
oracle.getAnchorBlockHash.mockResolvedValue(BlockHash.random());
258+
oracle.findLeafInsertionBlock.mockResolvedValue(undefined);
219259

220260
const builder = makeResetBuilder();
221261
expect(builder.needsReset()).toBe(true);
222262

223-
await expect(builder.build(oracle)).rejects.toThrow('Cannot find the leaf for nullifier');
263+
const err = await builder.build(oracle).then(
264+
() => undefined,
265+
(e: Error) => e,
266+
);
267+
expect(err?.message).toContain('Cannot find the leaf for nullifier');
268+
expect(err?.message).toContain('Anchor block 42');
269+
expect(err?.message).toContain('latest view: leaf not found');
224270
});
225271

226272
it('throws when pending read request has no matching nullifier', () => {

yarn-project/pxe/src/private_kernel/hints/private_kernel_reset_private_inputs_builder.ts

Lines changed: 119 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {
1010
import { makeTuple } from '@aztec/foundation/array';
1111
import { padArrayEnd } from '@aztec/foundation/collection';
1212
import type { Fr } from '@aztec/foundation/curves/bn254';
13+
import { createLogger } from '@aztec/foundation/log';
1314
import { assertLength } from '@aztec/foundation/serialize';
15+
import { sleep } from '@aztec/foundation/sleep';
1416
import { MembershipWitness } from '@aztec/foundation/trees';
1517
import { privateKernelResetDimensionsConfig } from '@aztec/noir-protocol-circuits-types/client';
1618
import {
@@ -37,16 +39,44 @@ import {
3739
getNullifierReadRequestResetActions,
3840
privateKernelResetDimensionNames,
3941
} from '@aztec/stdlib/kernel';
42+
import { MerkleTreeId } from '@aztec/stdlib/trees';
4043
import { type PrivateCallExecutionResult, collectNested } from '@aztec/stdlib/tx';
4144
import { VkData } from '@aztec/stdlib/vks';
4245

4346
import type { PrivateKernelOracle } from '../private_kernel_oracle.js';
4447

48+
// Settled-read membership witnesses are anchored at the tx's anchor block, whose data the node has already
49+
// synced, so a missing witness is either a transient node-side visibility race (a retry succeeds) or a
50+
// genuinely absent leaf (retries cannot mask it: the final error carries a latest-view probe to tell the
51+
// two apart). 3 total attempts, 300ms apart.
52+
const SETTLED_WITNESS_ATTEMPTS = 3;
53+
const SETTLED_WITNESS_RETRY_INTERVAL_MS = 300;
54+
55+
const log = createLogger('pxe:private-kernel-reset');
56+
57+
/** Thrown by the settled-read witness resolvers when the node cannot provide a witness at the anchor block. */
58+
class MissingSettledWitnessError extends Error {
59+
constructor(
60+
message: string,
61+
/** The tree the leaf was expected in. */
62+
public readonly treeId: MerkleTreeId,
63+
/** The leaf (siloed nullifier or siloed note hash) whose witness is missing. */
64+
public readonly leaf: Fr,
65+
) {
66+
super(message);
67+
this.name = 'MissingSettledWitnessError';
68+
}
69+
}
70+
4571
function getNullifierMembershipWitnessResolver(oracle: PrivateKernelOracle) {
4672
return async (nullifier: Fr) => {
4773
const res = await oracle.getNullifierMembershipWitness(nullifier);
4874
if (!res) {
49-
throw new Error(`Cannot find the leaf for nullifier ${nullifier}.`);
75+
throw new MissingSettledWitnessError(
76+
`Cannot find the leaf for nullifier ${nullifier}.`,
77+
MerkleTreeId.NULLIFIER_TREE,
78+
nullifier,
79+
);
5080
}
5181

5282
const { index, siblingPath, leafPreimage } = res;
@@ -57,6 +87,75 @@ function getNullifierMembershipWitnessResolver(oracle: PrivateKernelOracle) {
5787
};
5888
}
5989

90+
function getNoteHashMembershipWitnessResolver(oracle: PrivateKernelOracle) {
91+
return async (noteHash: Fr) => {
92+
const witness = await oracle.getNoteHashMembershipWitness(noteHash);
93+
if (!witness) {
94+
throw new MissingSettledWitnessError(
95+
`Read request is reading an unknown note hash ${noteHash}.`,
96+
MerkleTreeId.NOTE_HASH_TREE,
97+
noteHash,
98+
);
99+
}
100+
return witness;
101+
};
102+
}
103+
104+
/**
105+
* Appends the anchor block and a latest-view insertion probe to a missing-witness error, so a failure
106+
* self-reports whether the anchor predates the leaf (stale anchor), the node lost visibility of a leaf it
107+
* should serve (node inconsistency), or the leaf does not exist at all (bad read request).
108+
*/
109+
async function enrichMissingWitnessError(err: MissingSettledWitnessError, oracle: PrivateKernelOracle) {
110+
const anchorBlockNumber = oracle.getAnchorBlockNumber();
111+
let anchorBlockHash = 'unavailable';
112+
let latestView = 'latest-view probe failed';
113+
try {
114+
anchorBlockHash = (await oracle.getAnchorBlockHash()).toString();
115+
} catch {
116+
// Keep the fallback description.
117+
}
118+
try {
119+
const insertionBlock = await oracle.findLeafInsertionBlock(err.treeId, err.leaf);
120+
latestView = insertionBlock === undefined ? 'leaf not found' : `leaf inserted in block ${insertionBlock}`;
121+
} catch {
122+
// Keep the fallback description.
123+
}
124+
return new Error(
125+
`${err.message} Anchor block ${anchorBlockNumber} (hash ${anchorBlockHash}); latest view: ${latestView}.`,
126+
);
127+
}
128+
129+
/**
130+
* Runs a settled-read hint-building batch, retrying the whole batch when a witness the anchor block must
131+
* contain is transiently missing. Any error other than {@link MissingSettledWitnessError} rethrows untouched.
132+
*/
133+
async function buildSettledReadHintsWithRetry<T>(
134+
oracle: PrivateKernelOracle,
135+
description: string,
136+
buildHints: () => Promise<T>,
137+
): Promise<T> {
138+
for (let attempt = 1; ; attempt++) {
139+
try {
140+
return await buildHints();
141+
} catch (err) {
142+
if (!(err instanceof MissingSettledWitnessError)) {
143+
throw err;
144+
}
145+
if (attempt >= SETTLED_WITNESS_ATTEMPTS) {
146+
throw await enrichMissingWitnessError(err, oracle);
147+
}
148+
log.warn(`Missing settled ${description} witness at anchor block, retrying`, {
149+
attempt,
150+
maxAttempts: SETTLED_WITNESS_ATTEMPTS,
151+
anchorBlockNumber: oracle.getAnchorBlockNumber(),
152+
leaf: err.leaf.toString(),
153+
});
154+
await sleep(SETTLED_WITNESS_RETRY_INTERVAL_MS);
155+
}
156+
}
157+
}
158+
60159
async function getMasterSecretKeysAndKeyTypeDomainSeparators(
61160
keyValidationRequests: ClaimedLengthArray<
62161
ScopedKeyValidationRequestAndSeparator,
@@ -160,22 +259,26 @@ export class PrivateKernelResetPrivateInputsBuilder {
160259
const [previousVkMembershipWitness, noteHashReadRequestHints, nullifierReadRequestHints, keyValidationHints] =
161260
await Promise.all([
162261
oracle.getVkMembershipWitness(this.previousKernelOutput.verificationKey.keyAsFields),
163-
buildNoteHashReadRequestHintsFromResetActions<
164-
typeof MAX_NOTE_HASH_READ_REQUESTS_PER_TX,
165-
typeof MAX_NOTE_HASH_READ_REQUESTS_PER_TX
166-
>(
167-
oracle,
168-
this.previousKernel.validationRequests.noteHashReadRequests,
169-
this.previousKernel.end.noteHashes,
170-
this.noteHashResetActions,
262+
buildSettledReadHintsWithRetry(oracle, 'note hash', () =>
263+
buildNoteHashReadRequestHintsFromResetActions<
264+
typeof MAX_NOTE_HASH_READ_REQUESTS_PER_TX,
265+
typeof MAX_NOTE_HASH_READ_REQUESTS_PER_TX
266+
>(
267+
{ getNoteHashMembershipWitness: getNoteHashMembershipWitnessResolver(oracle) },
268+
this.previousKernel.validationRequests.noteHashReadRequests,
269+
this.previousKernel.end.noteHashes,
270+
this.noteHashResetActions,
271+
),
171272
),
172-
buildNullifierReadRequestHintsFromResetActions<
173-
typeof MAX_NULLIFIER_READ_REQUESTS_PER_TX,
174-
typeof MAX_NULLIFIER_READ_REQUESTS_PER_TX
175-
>(
176-
{ getNullifierMembershipWitness: getNullifierMembershipWitnessResolver(oracle) },
177-
this.previousKernel.validationRequests.nullifierReadRequests,
178-
this.nullifierResetActions,
273+
buildSettledReadHintsWithRetry(oracle, 'nullifier', () =>
274+
buildNullifierReadRequestHintsFromResetActions<
275+
typeof MAX_NULLIFIER_READ_REQUESTS_PER_TX,
276+
typeof MAX_NULLIFIER_READ_REQUESTS_PER_TX
277+
>(
278+
{ getNullifierMembershipWitness: getNullifierMembershipWitnessResolver(oracle) },
279+
this.previousKernel.validationRequests.nullifierReadRequests,
280+
this.nullifierResetActions,
281+
),
179282
),
180283
getMasterSecretKeysAndKeyTypeDomainSeparators(
181284
this.previousKernel.validationRequests.scopedKeyValidationRequestsAndSeparators,

yarn-project/pxe/src/private_kernel/private_kernel_oracle.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
UPDATES_VALUE_SIZE,
66
VK_TREE_HEIGHT,
77
} from '@aztec/constants';
8+
import type { BlockNumber } from '@aztec/foundation/branded-types';
89
import type { Fr } from '@aztec/foundation/curves/bn254';
910
import type { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin';
1011
import { MembershipWitness } from '@aztec/foundation/trees';
@@ -13,12 +14,13 @@ import { getVKIndex, getVKSiblingPath } from '@aztec/noir-protocol-circuits-type
1314
import { ProtocolContractAddress } from '@aztec/protocol-contracts';
1415
import type { FunctionSelector } from '@aztec/stdlib/abi';
1516
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
17+
import type { BlockHash } from '@aztec/stdlib/block';
1618
import { type ContractInstanceWithAddress, computeSaltedInitializationHash } from '@aztec/stdlib/contract';
1719
import { DelayedPublicMutableValues, DelayedPublicMutableValuesWithHash } from '@aztec/stdlib/delayed-public-mutable';
1820
import { computePublicDataTreeLeafSlot } from '@aztec/stdlib/hash';
1921
import type { AztecNode } from '@aztec/stdlib/interfaces/client';
2022
import { UpdatedClassIdHints } from '@aztec/stdlib/kernel';
21-
import type { NullifierMembershipWitness } from '@aztec/stdlib/trees';
23+
import type { MerkleTreeId, NullifierMembershipWitness } from '@aztec/stdlib/trees';
2224
import type { BlockHeader } from '@aztec/stdlib/tx';
2325
import type { VerificationKeyAsFields } from '@aztec/stdlib/vks';
2426

@@ -112,6 +114,26 @@ export class PrivateKernelOracle {
112114
return this.node.getNullifierMembershipWitness(await this.blockHeader.hash(), nullifier);
113115
}
114116

117+
/** The block number of the anchor block all membership queries of this oracle are anchored at. */
118+
getAnchorBlockNumber(): BlockNumber {
119+
return this.blockHeader.getBlockNumber();
120+
}
121+
122+
/** The hash of the anchor block, i.e. the reference block used for the node witness queries above. */
123+
getAnchorBlockHash(): Promise<BlockHash> {
124+
return this.blockHeader.hash();
125+
}
126+
127+
/**
128+
* Diagnostic lookup: the block that inserted the given leaf as of the node's latest view, if any.
129+
* Used to enrich missing-witness errors so a failure self-reports whether the leaf is missing entirely
130+
* or landed in a block past the anchor.
131+
*/
132+
async findLeafInsertionBlock(treeId: MerkleTreeId, leaf: Fr): Promise<BlockNumber | undefined> {
133+
const [result] = await this.node.findLeavesIndexes('latest', treeId, [leaf]);
134+
return result?.l2BlockNumber;
135+
}
136+
115137
/** Returns the root of our note hash merkle tree. */
116138
getNoteHashTreeRoot(): Fr {
117139
return this.blockHeader.state.partial.noteHashTree.root;

0 commit comments

Comments
 (0)