@@ -43,7 +43,11 @@ const SSTORE_SPAMMER = SPAM_CONFIGS[Opcode.SSTORE]![0]; // "Same slot (no limit)
4343jest . setTimeout ( 120_000 ) ;
4444
4545describe ( 'PublicProcessor C++ Timeout Race Condition' , ( ) => {
46- const NUM_ITERATIONS = 5 ;
46+ // BUG PROOF tests - this is the race condition and is flaky so we run more iterations
47+ const MAX_BUG_PROOF_ITERATIONS = 10 ;
48+ // FIX PROOF tests - just confirm that the fix always works
49+ const FIX_PROOF_ITERATIONS = 5 ;
50+
4751 const logger = createLogger ( 'public-processor-timeout-race' ) ;
4852
4953 const admin = AztecAddress . fromNumber ( 42 ) ;
@@ -70,10 +74,12 @@ describe('PublicProcessor C++ Timeout Race Condition', () => {
7074 * For the FIX proof: Call cancel(100) → wait for C++ to stop → then revert → no corruption
7175 *
7276 * @param useCancellation - Whether to call cancel() before reverts
77+ * @param numIterations - Number of iterations to run
7378 * @param spammer - Which spammer config to use (defaults to SSTORE for continuous writes)
7479 */
7580 async function runRaceConditionTest (
7681 useCancellation : boolean ,
82+ numIterations : number ,
7783 spamConfig : SpamConfig = SSTORE_SPAMMER , // Default to SSTORE for continuous writes
7884 ) : Promise < number > {
7985 let raceObservedCount = 0 ;
@@ -97,7 +103,7 @@ describe('PublicProcessor C++ Timeout Race Condition', () => {
97103 const contractAddress = contract . address ;
98104 const callArgs : Fr [ ] = [ ] ;
99105
100- for ( let iteration = 0 ; iteration < NUM_ITERATIONS ; iteration ++ ) {
106+ for ( let iteration = 0 ; iteration < numIterations ; iteration ++ ) {
101107 // Ensure any previous simulation is fully stopped before starting a new one
102108 await simulator . cancel ( 1000 ) ;
103109
@@ -154,11 +160,15 @@ describe('PublicProcessor C++ Timeout Race Condition', () => {
154160 if ( anyTreeCorrupted ) {
155161 raceObservedCount ++ ;
156162 // Early exit - bug exists, no need to continue
163+ // Always cancel simulation for clean test shutdown (prevent crash during afterEach)
164+ await simulator . cancel ( 1000 ) ;
157165 logger . verbose ( `Early exit` ) ;
158166 return raceObservedCount ;
159167 }
160168 }
161169
170+ // Always cancel simulation for clean test shutdown (prevent crash during afterEach)
171+ await simulator . cancel ( 1000 ) ;
162172 return raceObservedCount ;
163173 }
164174
@@ -173,8 +183,8 @@ describe('PublicProcessor C++ Timeout Race Condition', () => {
173183 * This test PASSES if we observe corruption (proving the bug exists).
174184 */
175185 it ( 'CppPublicTxSimulator BUG PROOF: race condition exists WITHOUT cancellation' , async ( ) => {
176- const raceObservedCount = await runRaceConditionTest ( false ) ;
177- logger . info ( `Race condition observed in ${ raceObservedCount } /${ NUM_ITERATIONS } iterations (expected: >0)` ) ;
186+ const raceObservedCount = await runRaceConditionTest ( false , MAX_BUG_PROOF_ITERATIONS ) ;
187+ logger . info ( `Race condition observed in >0 /${ MAX_BUG_PROOF_ITERATIONS } iterations (expected: >0)` ) ;
178188 expect ( raceObservedCount ) . toBeGreaterThan ( 0 ) ;
179189 } ) ;
180190
@@ -189,8 +199,8 @@ describe('PublicProcessor C++ Timeout Race Condition', () => {
189199 * This test PASSES if we observe NO corruption (proving the fix works).
190200 */
191201 it ( 'CppPublicTxSimulator FIX PROOF: no race condition WITH cancellation' , async ( ) => {
192- const raceObservedCount = await runRaceConditionTest ( true ) ;
193- logger . info ( `Race condition observed in ${ raceObservedCount } /${ NUM_ITERATIONS } iterations (expected: 0)` ) ;
202+ const raceObservedCount = await runRaceConditionTest ( true , FIX_PROOF_ITERATIONS ) ;
203+ logger . info ( `Race condition observed in ${ raceObservedCount } /${ FIX_PROOF_ITERATIONS } iterations (expected: 0)` ) ;
194204 expect ( raceObservedCount ) . toBe ( 0 ) ;
195205 } ) ;
196206
@@ -209,10 +219,12 @@ describe('PublicProcessor C++ Timeout Race Condition', () => {
209219 * Returns the number of times state corruption was observed.
210220 *
211221 * @param useCancellation - Whether to provide cancel() method to PublicProcessor
222+ * @param numIterations - Number of iterations to run
212223 * @param spammer - Which spammer config to use (defaults to SSTORE for continuous writes)
213224 */
214225 async function runPublicProcessorTimeoutTest (
215226 useCancellation : boolean ,
227+ numIterations : number ,
216228 spamConfig : SpamConfig = SSTORE_SPAMMER , // Default to SSTORE for continuous writes
217229 ) : Promise < number > {
218230 let corruptionCount = 0 ;
@@ -235,7 +247,7 @@ describe('PublicProcessor C++ Timeout Race Condition', () => {
235247 const contractAddress = contract . address ;
236248 const callArgs : Fr [ ] = [ ] ;
237249
238- for ( let iteration = 0 ; iteration < NUM_ITERATIONS ; iteration ++ ) {
250+ for ( let iteration = 0 ; iteration < numIterations ; iteration ++ ) {
239251 // Create fresh guarded tree and processor for each iteration because
240252 // GuardedMerkleTreeOperations.stop() is called on timeout and can't be reused.
241253 const guardedMerkleTrees = new GuardedMerkleTreeOperations ( merkleTrees ) ;
@@ -337,11 +349,16 @@ describe('PublicProcessor C++ Timeout Race Condition', () => {
337349 if ( anyTreeCorrupted ) {
338350 corruptionCount ++ ;
339351 // Early exit - bug exists, no need to continue
352+ // Always cancel simulation for clean test shutdown (prevent crash during afterEach)
353+ await realSimulator . cancel ( 1000 ) ;
340354 logger . verbose (
341355 `Early exit: checkWorldStateUnchanged caught=${ checkWorldStateUnchangedCaughtIt } , our check caught=true` ,
342356 ) ;
343357 return corruptionCount ;
344358 }
359+
360+ // Cancel simulation before next iteration or function end
361+ await realSimulator . cancel ( 1000 ) ;
345362 }
346363
347364 return corruptionCount ;
@@ -355,8 +372,8 @@ describe('PublicProcessor C++ Timeout Race Condition', () => {
355372 * cause of CI failures like "Fork state reference changed by tx after error".
356373 */
357374 it ( 'PublicProcessor BUG PROOF: state corruption occurs WITHOUT cancellation' , async ( ) => {
358- const corruptionCount = await runPublicProcessorTimeoutTest ( false ) ;
359- logger . info ( `State corruption detected in ${ corruptionCount } /${ NUM_ITERATIONS } iterations (expected: >0)` ) ;
375+ const corruptionCount = await runPublicProcessorTimeoutTest ( false , MAX_BUG_PROOF_ITERATIONS ) ;
376+ logger . info ( `State corruption detected in >0 /${ MAX_BUG_PROOF_ITERATIONS } iterations (expected: >0)` ) ;
360377 // BUG - Without cancellation, C++ corrupts state after process() completes
361378 expect ( corruptionCount ) . toBeGreaterThan ( 0 ) ;
362379 } ) ;
@@ -368,8 +385,8 @@ describe('PublicProcessor C++ Timeout Race Condition', () => {
368385 * State remains unchanged after process() returns.
369386 */
370387 it ( 'PublicProcessor FIX PROOF: no state corruption WITH cancellation' , async ( ) => {
371- const corruptionCount = await runPublicProcessorTimeoutTest ( true ) ;
372- logger . info ( `State corruption detected in ${ corruptionCount } /${ NUM_ITERATIONS } iterations (expected: 0)` ) ;
388+ const corruptionCount = await runPublicProcessorTimeoutTest ( true , FIX_PROOF_ITERATIONS ) ;
389+ logger . info ( `State corruption detected in ${ corruptionCount } /${ FIX_PROOF_ITERATIONS } iterations (expected: 0)` ) ;
373390 // FIX - With cancellation, state should remain unchanged
374391 expect ( corruptionCount ) . toBe ( 0 ) ;
375392 } ) ;
0 commit comments