Skip to content

Commit 0a88cf2

Browse files
authored
chore: public processor should not do slow Tx.clone - second attempt (#19221)
As shown in #19137, tx is never mutated even by e2e tests. I think this cloning was legacy because we _used_ to modify the Tx with contracts and effects I believe. Second attempt after #19154 was reverted due to flakes fixed in #19219 It turns out that `Tx.clone` was basically serving as a `sleep` that was letting us avoid this race condition.
1 parent 774f350 commit 0a88cf2

File tree

2 files changed

+32
-18
lines changed

2 files changed

+32
-18
lines changed

yarn-project/simulator/src/public/public_processor/apps_tests/timeout_race.test.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ const SSTORE_SPAMMER = SPAM_CONFIGS[Opcode.SSTORE]![0]; // "Same slot (no limit)
4343
jest.setTimeout(120_000);
4444

4545
describe('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
});

yarn-project/simulator/src/public/public_processor/public_processor.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export class PublicProcessor implements Traceable {
160160
let totalBlockGas = new Gas(0, 0);
161161
let totalBlobFields = 0;
162162

163-
for await (const origTx of txs) {
163+
for await (const tx of txs) {
164164
// Only process up to the max tx limit
165165
if (maxTransactions !== undefined && result.length >= maxTransactions) {
166166
this.log.debug(`Stopping tx processing due to reaching the max tx limit.`);
@@ -174,8 +174,8 @@ export class PublicProcessor implements Traceable {
174174
}
175175

176176
// Skip this tx if it'd exceed max block size
177-
const txHash = origTx.getTxHash().toString();
178-
const preTxSizeInBytes = origTx.getEstimatedPrivateTxEffectsSize();
177+
const txHash = tx.getTxHash().toString();
178+
const preTxSizeInBytes = tx.getEstimatedPrivateTxEffectsSize();
179179
if (maxBlockSize !== undefined && totalSizeInBytes + preTxSizeInBytes > maxBlockSize) {
180180
this.log.warn(`Skipping processing of tx ${txHash} sized ${preTxSizeInBytes} bytes due to block size limit`, {
181181
txHash,
@@ -187,7 +187,7 @@ export class PublicProcessor implements Traceable {
187187
}
188188

189189
// Skip this tx if its gas limit would exceed the block gas limit
190-
const txGasLimit = origTx.data.constants.txContext.gasSettings.gasLimits;
190+
const txGasLimit = tx.data.constants.txContext.gasSettings.gasLimits;
191191
if (maxBlockGas !== undefined && totalBlockGas.add(txGasLimit).gtAny(maxBlockGas)) {
192192
this.log.warn(`Skipping processing of tx ${txHash} due to block gas limit`, {
193193
txHash,
@@ -198,9 +198,6 @@ export class PublicProcessor implements Traceable {
198198
continue;
199199
}
200200

201-
// The processor modifies the tx objects in place, so we need to clone them.
202-
const tx = Tx.clone(origTx);
203-
204201
// We validate the tx before processing it, to avoid unnecessary work.
205202
if (preprocessValidator) {
206203
const result = await preprocessValidator.validateTx(tx);

0 commit comments

Comments
 (0)