The Architecture of a Split: Designing Phase 2 of the cuzk Proving Engine
Introduction
In the long arc of engineering complex systems, there comes a moment when the scaffolding is complete and the real architecture must be confronted. Message 399 of the cuzk coding session captures precisely such a moment. The assistant has just finished Phase 1—a working gRPC-based SNARK proving daemon capable of generating all four Filecoin proof types (WinningPoSt, WindowPoSt, SnapDeals, and PoRep C2) across multiple GPUs—and now stands at the threshold of Phase 2, the most technically demanding milestone in the project.
The message is a design deliberation. It is not a message that produces code, nor one that runs tests, nor one that validates outputs. It is a message where the assistant thinks aloud about architecture, weighing strategies, tracing call chains, and deciding how deep the fork must go. It is, in essence, a recorded moment of engineering judgment—and it reveals more about the craft of systems building than any clean commit message ever could.
The Context: What Phase 2 Demands
To understand why this message was written, one must understand what Phase 2 of the cuzk project requires. The cuzk proving engine, as designed in the earlier cuzk-project.md document, aims to transform the Filecoin SNARK proving pipeline from a monolithic, memory-hungry process into a continuous, pipelined, GPU-resident system. The core insight—discovered during the earlier deep-dive analysis of the SUPRASEAL_C2 pipeline—is that Groth16 proof generation naturally splits into two phases:
- Synthesis (CPU-bound): The circuit is constructed from the proof's input data. For Filecoin's stacked-depth-robust (SDR) circuits, this involves running the constraint system's
synthesize()method, which produces thea,b, andcevaluation vectors along with density trackers. This phase is entirely CPU work, dominated by SHA-256 hashing within the circuit constraints. - GPU computation (GPU-bound): The evaluation vectors are transformed via Number Theoretic Transforms (NTT) and Multi-Scalar Multiplications (MSM) on the GPU, ultimately producing the Groth16 proof. This phase is entirely GPU work, orchestrated by the supraseal C++/CUDA library. The problem is that in the existing codebase, these two phases are fused into a single monolithic function call. The caller invokes
seal_commit_phase2()(or the equivalent for PoSt proofs), and the entire proof is generated synchronously. The GPU is idle while synthesis runs, and the CPU is idle while the GPU runs. Moreover, the intermediate state—the synthesized assignments—occupies roughly 136 GiB of memory for a 32 GiB sector, and it is allocated, used, and discarded within a single function call. There is no opportunity to reuse it, pipeline it, or manage its lifetime. Phase 2's goal is to expose the seam between synthesis and GPU computation as a public API boundary, enabling the cuzk engine to: - Run synthesis on a CPU thread pool while the GPU is working on a different proof - Keep the GPU continuously fed with work (the "persistent prover" vision) - Reduce peak memory by streaming partitions through synthesis one at a time - Eventually support cross-sector batching and other advanced optimizations
The Message: A Deliberation on Strategy
Message 399 opens with the assistant reflecting on the task analysis results:
This is extremely helpful. Now I have the complete picture for Phase 2 design. Let me think about the optimal approach.
The assistant then lays out two competing strategies, each representing a fundamentally different philosophy of system integration.
Strategy A: Fork Bellperson Only
The first strategy is minimalism itself. The assistant observes that bellperson—the Rust library at the heart of Groth16 proving—already contains a clean internal separation. The function synthesize_circuits_batch() already exists as a private function; it just needs to be made public. Similarly, the ProvingAssignment struct that carries the synthesis output is crate-private; it just needs its visibility changed. A new function prove_from_assignments() would accept the synthesized state and run only the GPU phase.
Under this strategy, cuzk would continue to call through the existing filecoin-proofs-api layer for everything except the split. The pipeline would operate at the bellperson level: cuzk would call the existing API to get circuit objects, then call bellperson's new synthesize_circuits_batch() on a CPU thread, queue the result, and later call prove_from_assignments() on a GPU worker.
The assistant evaluates this strategy as requiring "minimal changes" but notes that "the pipeline operates at the bellperson level"—meaning cuzk would need to reach down through multiple abstraction layers to access this API.
Strategy B: Fork Bellperson + Bypass filecoin-proofs
The second strategy is more ambitious. Instead of just forking bellperson, the assistant considers forking the entire chain: storage-proofs-core (to add a synthesize_circuits_only() method to CompoundProof), filecoin-proofs (to add split API functions), and filecoin-proofs-api (to expose them). This would give cuzk full control over the pipeline at every level.
But the assistant immediately recognizes the cost: "That's 4 forks. Too many."
This moment of recognition—the instinctive recoil from excessive complexity—is the central design decision in this message. The assistant does not need to run an experiment or consult a document; it knows immediately that maintaining four forked upstream libraries is unsustainable. The judgment is instantaneous and correct.
The Reasoning: Trade-offs and Simplification
Having rejected Strategy B, the assistant returns to Strategy A but refines it. The key insight is that cuzk can replicate the small amount of glue code that connects filecoin-proofs-api to bellperson, rather than forking every layer. The assistant writes:
Simplest viable approach: Fork only bellperson. In cuzk-core, instead of callingfilecoin-proofs-api::seal_commit_phase2(), we replicate the small amount of glue code fromfilecoin-proofsthat: 1. Deserializes the C1 output into circuit structs (viaCompoundProof::circuit()) 2. Callsbellperson::groth16::synthesize_circuits_batch()(new pub API) 3. Returns the synthesized state
>
Then separately: 4. Calls bellperson::groth16::prove_from_assignments() (new pub API) on the GPU worker
This is the classic engineering trade-off: a small amount of code duplication (replicating the glue) in exchange for dramatically reduced maintenance burden (one fork instead of four). The assistant estimates "~100 lines of glue to cuzk-core"—a trivial cost compared to maintaining four separate forks across upstream API changes.
But this refined approach raises a new question: does cuzk have access to CompoundProof::circuit()? The CompoundProof trait and its implementations live in storage-proofs-core, storage-proofs-porep, storage-proofs-post, and storage-proofs-update. These are transitively pulled in by filecoin-proofs-api, but their APIs might not be publicly accessible from cuzk's perspective.
This question drives the final action in the message: the assistant spawns a sub-task to check the visibility of CompoundProof and related types:
[task] {"description":"Check CompoundProof visibility","prompt":"Thoroughness: medium\n\nI need to check the visibility of key types and functions that cuzk would need to call directly:\n\n1. In~/.cargo/registry/src/index.crates.io-*/storage-proofs-core-19.0.1/src/compound_proof.rs:\n - IsCompoundProof::circuit()public?..."
The message ends with this task dispatched, the design still open, the answer pending.
Assumptions and Their Implications
The message rests on several assumptions, some explicit and some implicit:
Assumption 1: The internal split in bellperson is sufficient. The assistant assumes that making synthesize_circuits_batch() public and adding prove_from_assignments() is all that's needed at the bellperson level. This is a reasonable assumption given the thorough analysis in the preceding messages, but it depends on the GPU phase being cleanly separable from the synthesis output. If the GPU phase modifies the assignments in place (e.g., for memory efficiency), the split might require deeper changes.
Assumption 2: The glue code is small. The assistant estimates ~100 lines to replicate the deserialization and circuit construction logic. This assumes that the circuit construction from C1 output is straightforward and doesn't involve complex state that's internal to filecoin-proofs. If the circuit construction requires access to private types or internal caching, the estimate could grow.
Assumption 3: CompoundProof::circuit() is public enough. The assistant is about to verify this, but the design depends on it. If circuit() is not public, or if it requires types that are themselves private, the approach of "replicating the glue" becomes harder—possibly requiring a second fork of storage-proofs-core.
Assumption 4: The fork will stay in sync. The assistant implicitly assumes that bellperson's API is stable enough that a fork can be maintained with minimal upstream drift. In practice, Filecoin's dependency tree is tightly versioned, and bellperson changes are rare, so this is a safe bet—but it's still an assumption.
The Thinking Process: A Window into Engineering Judgment
What makes this message remarkable is not the final answer—we don't even get one, because the message ends with a task dispatched—but the process of reasoning. The assistant walks through the design space methodically:
- State the goal: Split synthesis from GPU computation.
- Identify the seam: Bellperson already has
synthesize_circuits_batch()internally. - Enumerate strategies: Fork only bellperson vs. fork the entire chain.
- Evaluate each strategy: Minimal changes vs. full control, but 4 forks vs. 1 fork.
- Reject the complex strategy: "That's 4 forks. Too many."
- Refine the simple strategy: Can we avoid forking storage-proofs-core too?
- Identify the remaining unknown: Is
CompoundProof::circuit()public? - Dispatch a task to resolve the unknown. This is textbook engineering design: enumerate options, evaluate trade-offs, eliminate dominated alternatives, identify blocking questions, and defer decisions until the data is available. The assistant does not commit to a final design in this message because it recognizes that the answer to the visibility question changes the optimal approach.
Input Knowledge Required
To fully understand this message, one needs:
- Knowledge of Groth16 proving: Understanding what synthesis is, what the GPU phase does, and why splitting them is valuable.
- Knowledge of Filecoin's proof architecture: The layered structure of
filecoin-proofs-api→filecoin-proofs→storage-proofs-*→bellperson→supraseal-c2, and what each layer contributes. - Knowledge of the cuzk project's goals: The vision of a pipelined, GPU-resident proving engine that keeps the GPU continuously fed.
- Knowledge of the earlier analysis: The discovery that bellperson already has a clean internal split, documented in the preceding task results.
- Knowledge of Rust's visibility system: Understanding what
pubmeans, what crate-private means, and how[patch.crates-io]works for forking dependencies.
Output Knowledge Created
This message creates several forms of knowledge:
- A design decision tree: The two strategies and their trade-offs are explicitly documented. Even though no final decision is made, the space of possibilities is mapped.
- A cost estimate: The assistant estimates ~100 lines of glue code, giving a concrete sense of the implementation effort.
- A blocking question: The visibility of
CompoundProof::circuit()is identified as the critical unknown that determines the optimal strategy. - A task to resolve the unknown: The dispatched task will produce the missing information, enabling the final design decision in the next message.
- A record of engineering judgment: The rejection of the 4-fork approach ("Too many") is a qualitative judgment that shapes the entire Phase 2 architecture.
What This Message Reveals About the Assistant's Approach
The assistant's behavior in this message reveals several characteristics of its engineering style:
Conservatism in forking: The assistant is reluctant to fork upstream libraries. Each fork is a maintenance burden, and the assistant instinctively minimizes the number of forks. This is a mature engineering instinct—novice engineers often fork everything, while experienced engineers recognize that forks are liabilities.
Preference for minimal API changes: The assistant's fork of bellperson is described as "~30 lines of code in bellperson. No logic changes, just visibility." This is the ideal fork: it doesn't change behavior, it only exposes existing internals. Such forks are easy to maintain because they don't diverge from upstream behavior.
Recognition of the glue code trade-off: The assistant is willing to accept a small amount of code duplication (~100 lines) to avoid a much larger maintenance burden (4 forks). This is a rational trade-off that prioritizes long-term maintainability over short-term purity.
Systematic uncertainty resolution: When the assistant encounters an unknown (the visibility of CompoundProof::circuit()), it doesn't guess or assume—it dispatches a task to get the definitive answer. This prevents the design from being built on incorrect assumptions.
Conclusion
Message 399 is a design deliberation that never reaches a conclusion—and that is precisely its value. It captures the moment of decision-making before the decision is made, the weighing of alternatives before one is chosen, the identification of unknowns before they are resolved. In a coding session dominated by commits, tests, and benchmarks, this message stands out as a pure artifact of engineering thought.
The assistant's methodical walk through the design space, its instinctive rejection of excessive complexity, its willingness to accept small code duplication for large maintenance savings, and its systematic approach to resolving unknowns—all of these are the marks of a mature systems architect. The message may not produce a line of code, but it produces something more valuable: a clear, reasoned, and documented design rationale that will guide the entire Phase 2 implementation.
When the task result returns in the next message, the assistant will have the missing piece. The design will crystallize. The fork will be created. The pipeline will be built. But the architecture of that pipeline—the decision to fork only bellperson, to replicate the glue, to keep the maintenance burden minimal—was forged in this message, in the space between two strategies, in the moment of saying "Too many."