The Art of the Minimal Fork: Exposing Bellperson's Hidden Pipeline

A Single Edit That Unlocks Pipelined Proving

In the middle of a sprawling coding session spanning dozens of tool calls, hundreds of lines of analysis, and a 791-line design document, there is a message that appears almost trivial. At index 415, the assistant simply reports:

[edit] /home/theuser/curio/extern/bellperson/src/groth16/prover/mod.rs Edit applied successfully.

Seven words. A tool call confirmation. On its surface, this message is the least interesting thing in the conversation — just a notification that a file was edited. But this message is the culmination of an extraordinary chain of reasoning, and it represents one of the most consequential decisions in the entire cuzk project: the decision to fork bellperson with minimal changes, exposing an internal API boundary that was already there but hidden.

To understand why this message matters, we must understand everything that led to it.

The Context: A Year of Optimization

The cuzk project began as an investigation into the SUPRASEAL_C2 Groth16 proof generation pipeline for Filecoin's Proof-of-Replication (PoRep). The pipeline was known to consume approximately 200 GiB of peak memory, making it expensive to run on cloud GPU instances where memory is billed by the gigabyte. Over the course of seven segments and hundreds of messages, the assistant and user had developed five optimization proposals, implemented Phase 0 (the basic daemon architecture), validated Phase 0 with real GPU proofs achieving a 20.5% speedup from SRS residency, and completed Phase 1 by implementing all four Filecoin proof types with multi-GPU worker pool support.

Phase 2 was the next frontier: pipelined proving. The idea was to split the monolithic proof generation into two phases — CPU-intensive circuit synthesis and GPU-intensive proof computation — so that they could be overlapped, reducing peak memory and improving throughput. But this required modifying bellperson, the core proving library, which was an upstream dependency.

The Discovery: The Split Already Exists

The critical insight came in message 398, when the assistant analyzed bellperson's internal architecture and discovered that the synthesis/GPU split already existed internally. The function synthesize_circuits_batch() ran circuit synthesis on CPU using rayon-based parallelism, producing ProvingAssignment structures containing the a/b/c evaluation vectors and density trackers. Immediately after, a GPU phase packed these into raw pointer arrays and called supraseal_c2::generate_groth16_proof() for NTT + MSM + proof assembly on CUDA.

The boundary between these two phases was clean. The problem was that it was entirely private — synthesize_circuits_batch() was a private function, ProvingAssignment was a crate-private struct with private fields, and there was no public API to run synthesis without immediately proceeding to the GPU phase.

The assistant recognized this as an opportunity. Instead of rewriting bellperson's internals — a massive undertaking with high risk of introducing bugs — the assistant could simply expose what already existed. This was the "minimal-fork philosophy" in action: change only visibility modifiers, never logic.

The Message: A Surgical Edit

Message 415 is the second of two edits to prover/mod.rs. The first edit (message 414) had already made ProvingAssignment and its fields pub. This second edit makes two additional changes: it makes synthesize_circuits_batch() public (changing fn to pub fn), and it makes the supraseal submodule public (changing mod supraseal to pub mod supraseal under the #[cfg(feature = "cuda-supraseal")] conditional compilation guard).

These changes are deceptively simple. Each is a single keyword addition. But together, they transform bellperson from a monolithic proving library into a two-phase pipeline with a public API boundary. The supraseal module being public means external code can reach the prove_from_assignments() function that will be added in the next edit (message 416-417). The synthesize_circuits_batch() being public means external code can run synthesis independently.

The Reasoning: Why Minimalism Matters

The assistant's commitment to minimal changes was not arbitrary. It reflected a deep understanding of the software engineering trade-offs involved:

Risk reduction: Every line of modified logic is a potential bug. By changing only visibility modifiers, the assistant ensured that the fork's behavior was identical to upstream bellperson. The existing test suite — all 8 tests — would continue to pass without modification, providing immediate validation.

Maintainability: A fork with 130 lines of changes (across three files) is easy to rebase onto upstream updates. If bellperson releases a new version, the diff is small and mechanical. A fork with thousands of lines of rewritten logic would be a maintenance burden forever.

Reviewability: The changes are trivially reviewable. "Made struct fields pub" and "made function pub" are the simplest possible code review comments. There is no room for disagreement about correctness because no logic changed.

Philosophical alignment: The assistant had earlier considered forking four separate repositories (bellperson, storage-proofs-core, filecoin-proofs, filecoin-proofs-api) but rejected that approach as "too many forks." The single-fork strategy was the result of iterative refinement, starting from the most complex approach and stripping away unnecessary layers until only the essential remained.

Assumptions and Risks

The edit in message 415 rests on several assumptions, some explicit and some implicit:

That making ProvingAssignment fields public doesn't break invariants. The struct's fields — a, b, c evaluation vectors, density trackers, and input/auxiliary assignments — were previously only accessible within the prover module. Making them public means any external code can read and modify them. The assistant assumed that the GPU phase code (which reads these fields) would continue to work correctly, and that no internal invariant depended on field-level encapsulation. This was a reasonable assumption given that the fields are plain data structures with no internal consistency requirements, but it was not verified.

That the cuda-supraseal feature flag will be set. The supraseal module is conditionally compiled only when feature = "cuda-supraseal" is active. The assistant assumed that the cuzk workspace would enable this feature when using the fork. If the feature is not set, the public API surface would be different (the native prover would be used instead), and the prove_from_assignments() function would not exist. This assumption was validated later when the workspace was patched.

That pub(super) visibility on internal functions provides sufficient protection. The function create_proof_batch_priority_inner remained pub(super) — visible only within the prover module — even though the supraseal module was now pub. The assistant noted this explicitly: "external code could theoretically reach it, but since it's still pub(super) it's only visible within the prover module." This was correct: making a module pub doesn't change the visibility of items within it.

That the version numbering would work with Cargo's patch mechanism. This assumption proved incorrect. The assistant initially set the fork's version to 0.26.0-cuzk.1, but Cargo's [patch.crates-io] mechanism requires the patched version to satisfy the dependency requirement (which was 0.26.0). A pre-release version doesn't match in semver. This was discovered in message 422 and fixed by reverting to 0.26.0. This was the only significant mistake in the fork process, and it was caught and corrected within minutes.

Input Knowledge Required

To understand message 415, one must understand:

Rust visibility rules: The difference between fn (private), pub (public), pub(super) (visible to parent module), and crate-private. The entire fork strategy depends on these distinctions.

Cargo's patch mechanism: How [patch.crates-io] redirects dependencies to local forks. The assistant needed to know that the patched version must satisfy the semver requirement of the original dependency.

Bellperson's architecture: The existence of synthesize_circuits_batch() as a separate function from the GPU phase, the structure of ProvingAssignment, and the role of DensityTracker. This knowledge was acquired through the deep analysis in messages 398-400.

The supraseal C2 pipeline: How proof generation works end-to-end, from circuit synthesis through NTT/MSM on GPU to proof assembly. The assistant had spent segments 0-6 building this understanding.

The cuzk workspace structure: Where the fork would live (extern/bellperson/), how it would be integrated via [patch.crates-io], and which crates depend on bellperson transitively.

Output Knowledge Created

Message 415, combined with the edits before and after it, produced:

A forked bellperson with a public two-phase API: The synthesis phase (synthesize_circuits_batch) and the GPU phase (prove_from_assignments) are now callable from external code. This is the foundation for the pipelined prover.

A validated fork: All 8 existing tests pass. Zero new warnings. The fork compiles cleanly with the entire cuzk workspace.

A committed milestone: The fork was committed as f258e8c7 with a detailed commit message explaining every change. This provides a clean checkpoint for future reference.

A replicable pattern: The minimal-fork approach establishes a precedent for future modifications. If other upstream dependencies need modification, the same strategy can be applied.

The Broader Significance

Message 415 is a single edit to a single file, but it represents the culmination of an extraordinary analytical journey. The assistant had to:

  1. Understand the entire Filecoin proof generation pipeline, from Curio's Go task layer through Rust FFI into C++/CUDA kernels
  2. Identify the 200 GiB memory bottleneck and its root causes
  3. Design five optimization proposals, each with detailed impact analysis
  4. Implement Phase 0 and Phase 1 of the cuzk proving engine
  5. Analyze bellperson's internal architecture to find the synthesis/GPU split point
  6. Design the Phase 2 pipeline architecture with memory budget analysis
  7. Choose the minimal-fork strategy over more complex alternatives
  8. Execute the fork with surgical precision The edit itself is trivial. The reasoning behind it is not. This is the essence of good software engineering: the hard work is in the analysis, the design, and the decision-making. The actual code change is often the easiest part. The bellperson fork is now in place. The cuzk project is positioned to begin implementing the pipelined prover, with the synthesis/GPU split API providing the necessary foundation. The next steps — implementing the per-partition pipeline in cuzk-core, building the SRS manager, and validating against real proofs — will build on this foundation. But the critical architectural decision has been made, and it was made in messages like this one: small, precise, and backed by deep understanding.