Structural Parity in CuZK: Debugging the 196-Input Mismatch That Crashed WindowPoSt Proving

Introduction

In the high-stakes world of zero-knowledge proof systems, correctness is binary. Either the prover produces a valid proof, or it crashes with a structural mismatch that silently undermines the entire trust model. This article chronicles a debugging odyssey in the CuZK proving engine, a GPU-accelerated zero-knowledge proving system for Filecoin proof types. The journey began with an ambitious optimization—extending Pre-Compiled Constraint Evaluator (PCE) extraction to all proof types—and ended with a deep investigation into the ConstraintSystem trait hierarchy, where a single boolean flag and a subtle initialization mismatch conspired to crash WindowPoSt proving.

The PCE optimization separates circuit topology extraction from witness generation. Instead of re-synthesizing the entire R1CS constraint system for every proof—an expensive operation involving millions of constraint allocations—CuZK performs synthesis once per circuit type, recording the fixed constraint structure into a PreCompiledCircuit data structure. This structure is serialized to disk and reused for all subsequent proofs of the same type, allowing the GPU to evaluate constraints directly from this pre-compiled representation. The system uses two different constraint system implementations: RecordingCS for PCE extraction (faithfully recording every constraint into CSR matrix form) and WitnessCS for fast proving (tracking only witness assignments). Both implement the ConstraintSystem trait from the bellpepper-core library.

The assistant had successfully implemented PCE extraction for WinningPoSt, WindowPoSt, and SnapDeals proof types, extending beyond the original PoRep-only support. The changes compiled cleanly and were deployed for testing. But when the user tested WindowPoSt with PCE enabled, the prover crashed with a stark numerical mismatch: the witness had 26,036 inputs while the PCE expected only 25,840—a difference of exactly 196.

This article synthesizes the full arc of work in this segment: the implementation of PCE extraction across proof types, the partitioned SnapDeals pipeline, the methodical debugging of the WindowPoSt crash, and the structural fix that restored correct proving. Together, these episodes illustrate the complexity of extending optimization infrastructure across diverse proof types and the critical importance of maintaining strict structural consistency between extraction and execution paths in high-performance proving systems.

Background: The CuZK Proving Engine and PCE Extraction

Before diving into the implementation and debugging work, it is essential to understand the architecture of the CuZK proving engine and the role of PCE extraction within it.

CuZK is a GPU-accelerated zero-knowledge proving engine designed specifically for Filecoin proof types. Filecoin uses three primary proof types: PoRep (Proof of Replication), which proves that a miner is storing a unique copy of data; WinningPoSt (Proof of Spacetime), which proves that a miner has won the right to mine a block; WindowPoSt (Proof of Spacetime), which proves that a miner has continuously stored a set of sectors over a 24-hour window; and SnapDeals, which handles the replacement of committed sectors with new ones.

Each proof type involves a distinct circuit—a system of R1CS (Rank-1 Constraint System) constraints that encode the verification logic. Synthesizing these circuits is computationally expensive, involving millions of constraint allocations and variable assignments. The PCE optimization addresses this by performing synthesis once per circuit type, recording the fixed constraint structure (the "shape" of the circuit) into a pre-compiled representation that can be loaded directly onto the GPU for subsequent proofs.

The PCE extraction process uses RecordingCS, a constraint system implementation that faithfully records every constraint into Compressed Sparse Row (CSR) matrix form. This recorded structure captures the circuit topology—the pattern of constraints and variable connections—without the actual witness values. When a proof is later generated, the GPU can evaluate constraints directly from this pre-compiled structure, using the witness values provided separately.

The correctness of this optimization hinges on a critical invariant: the circuit topology recorded by RecordingCS must be structurally identical to the circuit topology that WitnessCS (the constraint system used for fast proving) would produce. Any divergence—any extra input, any missing constraint, any differently-indexed variable—will cause a crash at proving time, as the GPU expects a circuit structure that matches the witness it receives.

Implementing PCE Extraction for All Proof Types

The first phase of the work in this segment involved extending the extract_precompiled_circuit function and associated pipeline logic to handle WinningPoSt, WindowPoSt, and SnapDeals proof types. Originally, PCE extraction was implemented only for PoRep proofs. The challenge was to generalize this infrastructure to cover all Filecoin proof types, each with its own circuit structure, synthesis logic, and proving requirements.

For WinningPoSt, the circuit is relatively straightforward: it proves that a miner has won the right to mine a block by demonstrating that their storage power exceeds a threshold. The circuit dimensions are fixed and well-understood, making PCE extraction a natural fit.

For WindowPoSt, the circuit is more complex. WindowPoSt proves that a miner has continuously stored a set of sectors over a 24-hour window. The circuit must handle variable numbers of sectors per partition, with padding to a fixed maximum. The synthesis logic involves iterating over sectors, allocating public inputs for each sector's commitment, and constructing inclusion proofs. Crucially, the WindowPoSt circuit had two distinct synthesis paths based on the is_extensible() flag—a detail that would later prove to be the source of the crash.

For SnapDeals, the circuit handles the replacement of committed sectors with new ones, a mechanism that allows miners to dynamically manage their storage. SnapDeals introduces additional complexity because it must prove that the old sector data has been properly replaced and that the new sector data is correctly sealed.

The implementation work involved modifying the pipeline code in cuzk-core/src/pipeline.rs to register each proof type's circuit for PCE extraction, ensuring that the RecordingCS synthesis path was correctly invoked during the extraction phase. The changes compiled cleanly, a testament to the careful engineering of the codebase.

The Partitioned SnapDeals Pipeline

In addition to PCE extraction, the work introduced a partitioned pipeline for SnapDeals that overlaps circuit synthesis with GPU proving. This architecture, which had already been implemented for PoRep, works by dividing the circuit synthesis into independent chunks that can be synthesized in parallel, with each chunk being sent to the GPU for proving as soon as it is ready.

The partitioned pipeline is particularly valuable for SnapDeals because SnapDeals proofs involve multiple sector replacements, each of which can be synthesized independently. By overlapping synthesis and proving, the system can achieve significant reductions in wall-clock time. The analyzer summary estimates a ~43% reduction, which would be transformative for miners who need to generate SnapDeals proofs efficiently.

Implementing the partitioned pipeline required extending the synthesize_extendable path—the parallel synthesis logic that is activated when is_extensible() returns true—to handle the SnapDeals circuit structure. This involved defining how the circuit is split into chunks, how the chunks are synthesized independently, and how the results are merged into a complete witness.

A subagent was dispatched to conduct a separate but related investigation: could the Phase 7 partition pipeline—originally designed for PoRep C2 proofs—be generalized to handle SnapDeals and WindowPoSt? The subagent's analysis was exhaustive, classifying every component of the partition pipeline along the axes of generality and proof-type specificity.

The analysis revealed that much of the pipeline infrastructure was already fully generic: ProofAssembler, PartitionedJobState, SynthesizedProof, SynthesizedJob, and the GPU worker routing all contained no PoRep-specific fields or logic. The PoRep-specific components were concentrated in ParsedC1Output, parse_c1_output(), and build_partition_circuit_from_parsed(). For SnapDeals, the analysis concluded that generalization was both feasible and beneficial, requiring approximately 200-300 lines of new code. For WindowPoSt, the conclusion was surprising but well-reasoned: generalization was unnecessary because Curio already sends WindowPoSt proofs one partition at a time, making the partition pipeline's main innovation—overlapping synthesis of partitions from the same proof—inapplicable.

The WindowPoSt Crash: A Debugging Investigation

With the implementation complete and the changes compiled, the system was deployed for testing. The user enabled PCE for WindowPoSt and ran a proof. The result was a crash:

Witness produced 26036 inputs, PCE expected 25840

The difference of 196 inputs was precise and reproducible. The first WindowPoSt proof, which had been generated during PCE extraction, involved 102 sectors and produced a PCE with 25,840 inputs. The second proof, generated during fast proving, produced a witness with 26,036 inputs. Something was causing the two synthesis paths to diverge.

The user's first hypothesis was that the circuit dimensions might vary based on the number of sectors in a partition. If each sector contributed a fixed number of inputs, then a different sector count would explain the difference. The user commissioned a thorough investigation, laying out specific steps to trace the sector_count parameter through the circuit construction pipeline—from FallbackPoStCompound::circuit() through synthesize_window_post() to the FallbackPoSt circuit's synthesize() method.

The assistant began the investigation with broad file searches using glob and grep commands, locating the relevant code in pipeline.rs and engine.rs. These searches identified key functions like synthesize_window_post() and types like FallbackPoStCompound, providing entry points for deeper analysis.

However, a significant obstacle emerged: the storage-proofs-post crate containing the FallbackPoStCircuit was an external dependency from crates.io, not vendored locally. The assistant located the cached source files in the Cargo registry, but the execution environment restricted access to paths outside the project workspace. Every attempt to read, copy, or cat the files from the home directory failed. The assistant tried multiple workarounds—using bash commands, attempting to copy files to /tmp/czk, and searching for vendored copies—but all were blocked by the sandbox restrictions.

The breakthrough came through two parallel strategies. First, the assistant read the build dependency (.d) files in the target directory, which confirmed the exact source file paths for the compiled crate. Second, the assistant used webfetch to retrieve the source files directly from the GitHub mirror of the rust-fil-proofs repository. This dual approach—using build artifacts for path confirmation and remote fetching for content access—demonstrated the kind of lateral thinking required when working in constrained environments.

With the source files in hand (circuit.rs and compound.rs), the assistant began tracing the chain of alloc_input() calls. The FallbackPoStCircuit::synthesize() method dispatches based on whether the constraint system is extensible:

fn synthesize<CS: ConstraintSystem<Fr>>(self, cs: &mut CS) -> Result<(), SynthesisError> {
    if CS::is_extensible() {
        return self.synthesize_extendable(cs);
    }
    self.synthesize_default(cs)
}

This dispatch is the key to the entire bug. The synthesize_default method loops over sectors and calls each sector's synthesize, which in turn calls PoRCircuit::synthesize(). The alloc_input() calls happen through two paths: comm_r_num.inputize(cs) (which calls cs.alloc_input() once per sector) and multipack::pack_into_inputs(cs, &amp;auth_path_bits) (which packs Merkle authentication path bits into public inputs).

The critical finding came from the CompoundProof::circuit() implementation in compound.rs. The circuit construction always produces exactly num_sectors_per_chunk sectors, padding by repeating the last sector if the chunk has fewer real sectors. This means the k parameter (partition index) does not affect the number of alloc_input() calls—the public input count is fixed for a given circuit configuration. The padding mechanism ensures structural invariance regardless of which partition is being proven.

Root Cause: The is_extensible() Mismatch

With the circuit topology understood, the assistant could now focus on the actual crash. The witness had 26,036 inputs while the PCE expected 25,840—a difference of exactly 196 inputs. This precise number was a crucial clue.

The investigation revealed that RecordingCS (the constraint system used for PCE extraction) returned is_extensible() = false by default, while WitnessCS (the constraint system used for fast GPU-resident proving) returned true. The FallbackPoSt circuit's synthesize() method dispatches to different paths based on this flag. When is_extensible() is false, the circuit takes the synthesize_default path. When true, it takes the synthesize_extendable path, which splits work into parallel chunks, each allocating a "temp ONE" input. With the configured number of synthesis CPUs, this resulted in exactly 196 extra inputs—matching the crash delta perfectly.

The moment of discovery came when the assistant read the WitnessCS implementation and found is_extensible() returning true at lines 120-122, then checked RecordingCS and found no implementation at all, meaning it inherited the trait default of false. The assistant's message captured the realization: "There it is. RecordingCS: is_extensible() returns false (default). WitnessCS: is_extensible() returns true. The FallbackPoSt circuit dispatches to different synthesis paths based on is_extensible(). These two paths could produce different numbers of input allocations."

This was the root cause. The PCE extraction path (using RecordingCS) synthesized a circuit with num_inputs = 25,840, while the fast proving path (using WitnessCS) synthesized a circuit with num_inputs = 26,036. The extra 196 inputs corresponded exactly to the number of synthesis chunks configured for parallel synthesis—each chunk added one "temp ONE" input that was not present in the non-extensible path.

This was a classic example of what happens when two code paths that are supposed to be semantically equivalent diverge due to a seemingly minor trait implementation detail. The is_extensible() flag, which was designed to enable performance optimization, had inadvertently created a correctness bug.## The Fix: Making RecordingCS Fully Extensible

The fix strategy was clear: make RecordingCS fully extensible to mirror WitnessCS behavior. This required implementing three things: is_extensible() returning true, an extend() method with proper CSR column index remapping, and ensuring the constructor initialized identically.

The assistant began by reading the full RecordingCS source file, understanding its CSR matrix format where input variables are stored as raw indices and auxiliary variables carry an AUX_FLAG bit. The extend() method needed to handle this encoding carefully: when merging a child constraint system into the parent, every column index referencing a child variable must be remapped to the corresponding parent variable index. Input indices needed one offset, aux indices another, and the child's built-in ONE variable (input 0) had to map to the parent's existing ONE rather than being duplicated.

The assistant articulated the plan: "Now I need to make RecordingCS extensible. The key methods I need to implement are: 1. is_extensible()true, 2. extend(&amp;mut self, other: &amp;Self) — merge another RecordingCS into this one, remapping variable indices, 3. The new() constructor already exists." The assistant immediately flagged the hardest part: "The tricky part is extend — when merging CSR matrices from the child CS into the parent, variable indices for aux and input need to be offset by the parent's current counts."

The implementation enumerated five steps for the extend method: skip the child's first input (index 0 = "ONE"), remap child input indices via child_input_idx - 1 + self.num_inputs, remap child aux indices via child_aux_idx + self.num_aux, append constraints with remapped columns, and update counts.

The Critical Insight: Variable Remapping

But the assistant then paused to reconsider a critical assumption. The assistant realized that RecordingCS::extend could not simply mirror WitnessCS::extend because of a fundamental difference between the two systems: in WitnessCS::extend, the first input is skipped from the assignment vectors but the constraint system doesn't have constraints in WitnessCS (enforce is a no-op). In RecordingCS, constraints DO reference variables. A child constraint referencing child input 0 (its local ONE) should map to parent input 0 (the parent's ONE).

This was a crucial insight. WitnessCS does not record constraints—its enforce method is a no-op. It only tracks assignments. RecordingCS, on the other hand, faithfully records every constraint into CSR matrices. When a constraint in a child chunk references the child's local ONE variable (input 0), that reference must be remapped to the parent's ONE (input 0), not skipped entirely. The assistant corrected the implementation accordingly.

The Temp ONE Problem and Initialization Mismatch

The deepest subtlety emerged when the assistant traced through the variable indexing in the synthesize_extendable flow and discovered a fundamental initialization mismatch. In WitnessCS::new(), the constructor pre-allocates input_assignment = vec![Scalar::ONE], making the built-in ONE variable occupy index 0 from the start. When synthesize_extendable then calls cs.alloc_input(|| &#34;temp ONE&#34;, || Ok(Fr::ONE)), the "temp ONE" lands at index 1.

But RecordingCS::new() called new_empty(), which set num_inputs = 0 and allocated no variables. When synthesize_extendable called cs.alloc_input(|| &#34;temp ONE&#34;, || Ok(Fr::ONE)), the "temp ONE" landed at index 0—there was no built-in ONE to skip. The child RecordingCS had its "temp ONE" at index 0, while the child WitnessCS had its "temp ONE" at index 1.

The assistant's realization was sharp: "This is a fundamental mismatch. RecordingCS::new() doesn't pre-allocate a ONE variable, but WitnessCS::new() does. I need to make RecordingCS::new() also pre-allocate a ONE input to match."

The Ripple Effect: Coordinated Changes

But changing new() had consequences. The main extraction function extract_precompiled_circuit() also called RecordingCS::new() and then manually allocated ONE:

let mut cs = RecordingCS::<Scalar>::new();
cs.alloc_input(|| "one", || Ok(Scalar::ONE))?;

If new() now pre-allocated ONE (setting num_inputs = 1), then the manual alloc_input would push it to 2, double-counting the ONE variable. The assistant recognized this ripple effect: "But wait — this changes the behavior of the main extraction path too."

The assistant considered alternatives—not changing new(), or only changing it for the extensible path—but dismissed them because synthesize_extendable calls CS::new() generically through the trait interface. There was no way to give the main path a different constructor from the child path without changing the circuit code itself. The correct solution was to make RecordingCS::new() match WitnessCS::new() (pre-allocate ONE) and update extract_precompiled_circuit() to remove the now-redundant manual allocation.

The edit removed the redundant allocation, completing the transformation of RecordingCS into a faithful mirror of WitnessCS.

Verification Through Reasoning

The build compiled cleanly, with only pre-existing warnings about private interface visibility in cuzk-core. But the assistant did not stop at compilation. The assistant performed a meticulous correctness verification, tracing through the entire execution flow to ensure structural parity.

The assistant examined the input constraints loop in extract_precompiled_circuit:

for i in 0..num_inputs {
    cs.enforce(|| "", |lc| lc + Variable(Index::Input(i)), |lc| lc, |lc| lc);
}

This loop adds an input_i * 1 = 0 constraint for every input, including the pre-allocated ONE at index 0. The assistant traced through both the old and new initialization paths, confirming that the final num_inputs count was identical in both cases. In the old code, new_empty() set num_inputs = 0, then alloc_input(&#34;one&#34;) set it to 1, then circuit synthesis added more. In the new code, new() set num_inputs = 1 directly, then circuit synthesis added the same number. The first circuit-allocated input got index 1 in both scenarios.

The assistant then traced through the synthesize_extendable flow for RecordingCS step by step:

  1. Parent RecordingCS starts with num_inputs = 1 (ONE at index 0)
  2. extract_precompiled_circuit calls circuit.synthesize(&amp;mut cs)
  3. The circuit is FallbackPoStCircuit, is_extensible() = true, so → synthesize_extendable
  4. For each chunk: CS::new() → child RecordingCS with num_inputs = 1 (ONE at index 0); cs.alloc_input(&#34;temp ONE&#34;) → child num_inputs = 2, "temp ONE" at index 1; sector synthesis adds inputs starting at index 2
  5. cs.extend(&amp;sector_cs): Skip child input 0 (ONE); child input 1 ("temp ONE") → parent input input_offset + 0; child input 2... → parent input input_offset + 1...; self.num_inputs += other.num_inputs - 1 The assistant concluded: "This matches what WitnessCS does. The final num_inputs count will include the temp ONE from each chunk—matching the WitnessCS behavior."

Broader Lessons for GPU-Resident Proving

This debugging episode reveals several important lessons for engineers working with high-performance proving systems.

Trait Consistency Is Correctness

In Rust, trait implementations define the contract between components. When two implementations of the same trait diverge in their behavior—even on a seemingly minor flag like is_extensible()—the consequences can be catastrophic. The ConstraintSystem trait is the backbone of bellperson's synthesis pipeline; any inconsistency between implementations breaks the fundamental assumption that circuit topology is independent of the constraint system used to synthesize it.

The ConstraintSystem trait provides a default is_extensible() implementation returning false. This default exists precisely because extend() is not universally implemented. However, assuming that a constraint system implementation can safely use the default without understanding the circuit's dispatch logic proved catastrophic. The FallbackPoStCircuit actively checks is_extensible() and branches accordingly—a design that assumes all constraint systems in a given proving pipeline will agree on this flag.

Structural Isomorphism Is Non-Negotiable

When a proving system uses two different constraint system implementations—one for circuit topology extraction and one for witness synthesis—they must produce structurally identical circuits. Any divergence, no matter how seemingly minor, will cause a crash at proving time. The is_extensible() flag was a small trait method, but its behavioral difference had catastrophic consequences.

The PCE optimization works by separating circuit structure from circuit values—capturing the topology once and evaluating it many times with different witnesses. But this separation only works if the topology capture is faithful: the recorded structure must exactly match the structure that the prover will use. Any divergence, no matter how subtle, produces incorrect proofs or crashes.

Constructor Semantics Must Be Uniform

The assistant initially assumed that CS::new() would produce equivalent initial states across implementations. In reality, WitnessCS::new() pre-allocates the ONE variable as a convenience for witness generation, while RecordingCS::new_empty() starts from zero. This divergence is invisible during normal operation because each CS type is used independently—it only becomes visible when the circuit dispatches to synthesize_extendable, which creates child CS instances of the same type as the parent and then merges them back.

This initialization mismatch was the most subtle aspect of the bug. Even if is_extensible() had been correctly implemented on RecordingCS from the start, the constructor divergence would have caused a different crash—the "temp ONE" would have been at index 0 in the child RecordingCS but at index 1 in the child WitnessCS, leading to incorrect variable remapping during extend().

Performance Optimizations Can Create Correctness Bugs

The is_extensible() flag was introduced to enable parallel synthesis, a performance optimization. But because it was not consistently implemented across both constraint system types, it created a divergence between the extraction and proving paths. This is a recurring pattern in high-performance systems: optimizations that are applied asymmetrically can break invariants that the rest of the system depends on.

The partitioned pipeline for SnapDeals, while promising significant performance improvements, introduced additional complexity into the system. The generalization of the partition pipeline required careful analysis to ensure that the optimization did not introduce structural divergences between proof types. The subagent's analysis of which components were generic and which were proof-type-specific was essential to making this generalization safely.

Fixing One Code Path Often Requires Updating Another

The change to RecordingCS::new() had a ripple effect on extract_precompiled_circuit(), which had been written assuming the old constructor behavior. The assistant's recognition of this dependency and its willingness to update both paths consistently was essential to the fix's correctness.

This is a general principle of software engineering: when fixing a bug that involves a structural invariant, all code paths that depend on that invariant must be examined and updated. In this case, the invariant was "the ONE variable is always at index 0 in a freshly constructed constraint system." The extraction function had been written to manually allocate ONE because RecordingCS::new() did not do so. Once new() was fixed, the manual allocation became a bug—it would have created a duplicate ONE at index 1, shifting all subsequent variable indices and causing a different mismatch.

The Value of Methodical Debugging

The debugging process for this crash was exemplary: the user observed the symptom (196-input mismatch), formulated a hypothesis (variable sector count), designed a systematic investigation, and only when that hypothesis was eliminated did the team trace the bug to the constraint system trait hierarchy. This disciplined approach prevented wasted effort chasing phantom sector count variations.

The assistant's use of subagents for deep investigation was a strategic choice that enabled focused, multi-turn exploration without losing context. The subagents could trace code paths across crate boundaries, examine trait implementations, and reason about parallel synthesis behavior—all within dedicated sessions that ran to completion independently.

Conclusion

The crash that began with a 196-input mismatch was resolved through a series of coordinated changes to RecordingCS: implementing is_extensible() to return true, implementing extend() with proper CSR column index remapping, pre-allocating the built-in ONE variable in new(), and removing the redundant manual ONE allocation from extract_precompiled_circuit(). Each change was necessary; omitting any one would have left the system in an inconsistent state.

The extension of PCE extraction to support WinningPoSt, WindowPoSt, and SnapDeals proof types represents a significant step forward for the CuZK proving engine. The partitioned SnapDeals pipeline, which overlaps synthesis and GPU proving, promises substantial performance improvements for miners. And the debugging of the WindowPoSt crash, while challenging, ultimately strengthened the system by revealing and fixing a subtle structural divergence between the two constraint system implementations.

The 196-input mismatch that initially appeared to be a mysterious crash was, in retrospect, a clear signal: the is_extensible() flag was not just a performance optimization toggle, but a fundamental structural property of the constraint system that had to be consistent across all implementations. By tracing the bug to its root cause and implementing a comprehensive fix—making RecordingCS fully extensible, correcting its initialization, and updating the extraction pipeline—the team ensured that the PCE extraction path and the fast proving path would always produce identical circuit topologies.

This episode underscores a core principle of zero-knowledge proving infrastructure: correctness depends on strict consistency between the circuit topology used for pre-computation and the circuit topology used for actual proving. Any divergence, no matter how subtle, will manifest as a crash at the worst possible moment. The WindowPoSt crash was a hard lesson, but it was also a valuable one—it revealed a structural weakness in the system and provided the impetus to fix it properly.

For practitioners working on similar systems, this session offers a valuable case study in the importance of trait consistency, the value of methodical debugging, and the necessity of understanding the full synthesis path from circuit definition to constraint system implementation. The 196-input mismatch was not a random error—it was a precise signal pointing directly to the root cause, if only one knew how to read it.

References

[1] Chunk 0: "The Extensibility Bug: How a Single Boolean Flag Crashed WindowPoSt Proving in CuZK" — Analysis of the PCE extraction implementation and the initial debugging investigation.

[2] Chunk 1: "Structural Parity in Zero-Knowledge Proving: How a Single Boolean Flag Crashed WindowPoSt and What It Took to Fix It" — Detailed analysis of the fix implementation and the initialization mismatch.

[3] Subagent analysis of the Phase 7 partition pipeline generalization for SnapDeals and WindowPoSt.

[4] The extract_precompiled_circuit function and its role in PCE extraction for all proof types.

[5] The FallbackPoStCircuit::synthesize() dispatch logic and the two synthesis paths.

[6] The WitnessCS::is_extensible() implementation and its impact on input allocation.

[7] The RecordingCS CSR matrix format and the challenges of implementing extend().

[8] The initialization mismatch between RecordingCS::new() and WitnessCS::new().

[9] The ripple effect of changing RecordingCS::new() on extract_precompiled_circuit().

[10] The methodical debugging approach: implement, test, observe failure, trace root cause, implement fix, verify through reasoning.