Skip to content

fix(simulator): make circuit recorder concurrency-safe via AsyncLocalStorage#24112

Merged
vezenovm merged 6 commits into
merge-train/fairies-v5from
mv/fix-circuit-recorder-finish-undefined
Jun 16, 2026
Merged

fix(simulator): make circuit recorder concurrency-safe via AsyncLocalStorage#24112
vezenovm merged 6 commits into
merge-train/fairies-v5from
mv/fix-circuit-recorder-finish-undefined

Conversation

@vezenovm

@vezenovm vezenovm commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Holds the active circuit recording in AsyncLocalStorage instead of shared instance fields, so nested and concurrent executions no longer corrupt each other's recording.

Problem:

CircuitRecorder kept mutable state (recording/stackDepth/newCircuit) on one per-PXE instance. Nested re-entrant executions (nested private calls via aztec_prv_callPrivateFunction, utility executions) shared that state, so a child could reset the parent's recording mid-flight and drop or misattribute oracle calls. The error path then dereferenced the absent recording and masked the real failure (e.g. schnorr_initializerless: capsule load failed). The first two commits guarded the crash; this fixes the cause and leaves the guards as redundant defense.

Fix:

  • Per-execution recording lives in AsyncLocalStorage (mirrors foundation/src/profiler/profiler.ts)
  • record(metadata, fn) replaces start/finish/finishWithError and re-throws the original error unchanged.
  • No CircuitSimulator interface or consumer change.

Tests: oracle-call attribution across nested private + utility re-entries (red before this change: parent oracles came back undefined), isolation of interleaved concurrent recordings, and original simulator error surfacing without masking.

Unblocks #23866

CircuitRecorder.finish() and finishWithError() dereferenced this.recording without a guard, so finalizing with no active recording threw "Cannot read properties of undefined (reading 'parent')". Because SimulatorRecorderWrapper calls finishWithError on the error path before re-throwing, that TypeError replaced the real failure (e.g. schnorr_initializerless: capsule load failed).

Guard both methods (and the FileCircuitRecorder overrides) to resolve to undefined when there is no recording, so the original error propagates.
@vezenovm vezenovm requested review from Thunkar, mverzilli and nchamo June 15, 2026 22:42
recordCall was the remaining unguarded this.recording! deref (companion to the finish/finishWithError guards): this.recording!.oracleCalls.push(entry). Under concurrent reuse of the shared recorder, this.recording can be reset mid-flight, so recordCall threw "Cannot read properties of undefined (reading 'oracleCalls')" AFTER the real oracle had already returned, fabricating a failure in an otherwise-successful execution (then surfaced via the wrapper's catch).

Record best-effort and never throw: skip the push when there is no active recording. Also guards FileCircuitRecorder.recordCall and start (the file-recording-on path). The recorder is debug/profiling-only; making recordings concurrency-correct (no misparenting/dropping) is a separate follow-up.
@vezenovm vezenovm marked this pull request as draft June 16, 2026 04:01
@vezenovm vezenovm changed the title fix(simulator): prevent circuit recorder from masking execution errors fix(simulator): make circuit recorder concurrency-safe via AsyncLocalStorage Jun 16, 2026
@vezenovm vezenovm marked this pull request as ready for review June 16, 2026 04:08
@@ -0,0 +1,9 @@
import { readFileSync } from 'node:fs';

describe('client entrypoint', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like too much

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, removed

@Thunkar Thunkar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much cleaner, but unfortunately browser incompatible. Still, this feature is relatively niche, so LGTM

@vezenovm vezenovm enabled auto-merge (squash) June 16, 2026 14:17
@vezenovm

Copy link
Copy Markdown
Contributor Author

Much cleaner, but unfortunately browser incompatible. Still, this feature is relatively niche, so LGTM

Yeah unfortunately. This at least unblocks #23866 but we would need a bigger refactor which didn't feel necessary at this stage.

@vezenovm vezenovm merged commit 5e239c7 into merge-train/fairies-v5 Jun 16, 2026
15 checks passed
@vezenovm vezenovm deleted the mv/fix-circuit-recorder-finish-undefined branch June 16, 2026 14:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants