@@ -10,7 +10,9 @@ import {
1010import { makeTuple } from '@aztec/foundation/array' ;
1111import { padArrayEnd } from '@aztec/foundation/collection' ;
1212import type { Fr } from '@aztec/foundation/curves/bn254' ;
13+ import { createLogger } from '@aztec/foundation/log' ;
1314import { assertLength } from '@aztec/foundation/serialize' ;
15+ import { sleep } from '@aztec/foundation/sleep' ;
1416import { MembershipWitness } from '@aztec/foundation/trees' ;
1517import { privateKernelResetDimensionsConfig } from '@aztec/noir-protocol-circuits-types/client' ;
1618import {
@@ -37,16 +39,44 @@ import {
3739 getNullifierReadRequestResetActions ,
3840 privateKernelResetDimensionNames ,
3941} from '@aztec/stdlib/kernel' ;
42+ import { MerkleTreeId } from '@aztec/stdlib/trees' ;
4043import { type PrivateCallExecutionResult , collectNested } from '@aztec/stdlib/tx' ;
4144import { VkData } from '@aztec/stdlib/vks' ;
4245
4346import 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+
4571function 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+
60159async 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 ,
0 commit comments