The Moment the Bellperson Fork Took Shape: Exposing Internal APIs for the Pipelined Prover

"Now modify supraseal.rs — make synthesize_circuits_batch public and add prove_from_assignments"

At first glance, message [msg 416] appears unremarkable: a single tool invocation that edits a Rust source file, acknowledged with the terse confirmation "Edit applied successfully." But this message represents the critical inflection point where weeks of architectural analysis, design documentation, and careful planning crystallized into concrete code. It is the moment the bellperson fork — the foundation upon which Phase 2 of the cuzk proving engine would be built — received its defining API surface.

Context: The Long Road to a Minimal Fork

To understand why this message matters, one must trace the arc of the cuzk project. The cuzk proving engine was conceived as a pipelined SNARK proving daemon for Filecoin's storage proofs, designed to address the structural inefficiencies of the existing proof pipeline — chief among them the ~200 GiB peak memory footprint of Groth16 proof generation and the repeated overhead of loading Structured Reference Strings (SRS) from disk for each proof.

By the time the assistant reached [msg 416], it had already completed Phase 0 (the core daemon architecture with gRPC API, priority scheduler, and end-to-end validation) and Phase 1 (implementing all four Filecoin proof types — WinningPoSt, WindowPoSt, SnapDeals, and PoRep — along with multi-GPU worker pool support and test data generation). The gen-vanilla command had been built and validated against golden test data, producing correct outputs (164 KB WinningPoSt proof, 25 KB WindowPoSt proof, 12 MB SnapDeals partition proofs).

The critical insight that made Phase 2 possible emerged during the analysis documented in [msg 398] through [msg 400]: bellperson's supraseal prover already had a clean two-phase architecture internally. The function synthesize_circuits_batch() performed CPU-only circuit synthesis, returning ProvingAssignment structures containing the a, b, c evaluation vectors. The GPU phase — NTT, MSM, and proof assembly — followed in create_proof_batch_priority_inner. These two phases were separated by a clean boundary, but both were private to the module. No external caller could invoke them independently.

This discovery was transformative. It meant that the pipelined prover did not need to rewrite bellperson's synthesis logic or GPU orchestration. It needed only visibility — the ability to call synthesis on one thread, queue the result, and call the GPU phase on another thread. The fork could be minimal: roughly 130 lines of changes, no logic modifications, just pub annotations and a thin wrapper function.

What Message 416 Actually Did

The edit targeted /home/theuser/curio/extern/bellperson/src/groth16/prover/supraseal.rs, the file containing the supraseal-backed prover implementation. Two changes were applied simultaneously:

First, the existing synthesize_circuits_batch() function was changed from private (or pub(super)) to fully pub. This function takes a vector of circuit instances and their inputs, runs the bellpepper-core constraint synthesis for each circuit in parallel using rayon, and returns a tuple of (Vec<ProvingAssignment<Scalar>>, Vec<Arc<Vec<Scalar>>>, Vec<Arc<Vec<Scalar>>>) — the assignments, input witnesses, and auxiliary witnesses. Making this public meant that cuzk-core could call synthesis directly, without going through the monolithic create_random_proof_batch entry point.

Second, a new public function prove_from_assignments() was added. This function extracts the GPU-phase code from the existing create_proof_batch_priority_inner, accepting pre-synthesized ProvingAssignment values and running only the CUDA-backed NTT and MSM operations needed to produce the final Groth16 proof. This is the function that would run on the GPU worker pool, isolated from the CPU synthesis threads.

The edit was preceded by two other edits in the same round ([msg 414] and [msg 415]) that modified prover/mod.rs to make the ProvingAssignment struct and its fields public, and to make the supraseal submodule itself public. Together, these three edits formed the complete API surface of the bellperson fork.

The Reasoning Behind the Design

The assistant's approach reflects a deliberate philosophy of minimal intervention. Rather than forking multiple upstream crates (storage-proofs-core, filecoin-proofs, filecoin-proofs-api), the design confines all changes to a single crate: bellperson. The rationale, articulated in [msg 400], is that CompoundProof::circuit() is already a public trait method, and all the compound proof types (StackedCompound, FallbackPoStCompound, EmptySectorUpdateCompound) are already public. Cuzk-core can construct circuit instances directly using these public APIs, then call the newly-exposed bellperson functions for synthesis and proving. No changes to the proof-type crates are needed.

This decision carries an implicit assumption: that the public API of storage-proofs-core and its sibling crates is sufficient to construct circuit instances without internal knowledge. The assistant verified this assumption in [msg 399] by tracing the visibility of CompoundProof::circuit() and confirming it is indeed a public trait method. Had this not been the case, the fork would have needed to expand to additional crates, increasing maintenance burden and the risk of merge conflicts.

A Subtle Mistake and Its Resolution

The assistant's reasoning was sound, but one detail would soon surface as a problem. The fork's Cargo.toml initially specified version 0.26.0-cuzk.1 — a pre-release version identifier. When the workspace patch was applied in [msg 420], Cargo's semver resolution refused to match 0.26.0-cuzk.1 against the dependency requirement bellperson = "0.26.0". The patch was silently ignored, as [msg 421] revealed: "Patch bellperson v0.26.0-cuzk.1 was not used in the crate graph."

This mistake, while minor, illustrates the gap between conceptual design and practical implementation. The assistant had correctly identified every architectural requirement but overlooked the mechanical detail of Cargo's version resolution semantics. The fix, applied in [msg 422], was trivial — reverting the version to 0.26.0 — but without the diagnostic feedback from the build system, the fork would have compiled in isolation while the workspace continued using the unmodified upstream bellperson, silently defeating the purpose of the entire exercise.

The Broader Significance

Message [msg 416] is the moment when the cuzk project transitioned from design to implementation for its most consequential phase. The pipelined prover architecture described in the 791-line cuzk-phase2-design.md document depended entirely on the ability to split synthesis from proving. Without the bellperson fork, that architecture remained theoretical. With it, the path was clear: cuzk-core could spawn synthesis tasks on CPU threads, queue the resulting ProvingAssignment values, and dispatch them to GPU workers — all while maintaining the priority scheduling and job management infrastructure built in Phase 0.

The fork's minimalism is worth emphasizing. The assistant did not rewrite bellperson's synthesis loop, did not modify the GPU kernel dispatch, and did not alter the proof serialization format. It exposed existing internals and added a single thin wrapper. This restraint reduces the long-term maintenance burden: when upstream bellperson releases new versions, the fork's changes can be ported by re-applying roughly 130 lines of visibility annotations, rather than reconciling divergent codebases.

The subsequent compilation check in [msg 423] confirmed that the fork compiled cleanly, and the test run in [msg 424] verified that all 8 existing bellperson tests continued to pass. The fork introduced no regressions. The commit f258e8c7 would mark this milestone, positioning the project to begin implementing the pipelined prover in cuzk-core — the core deliverable of Phase 2 and the culmination of the cuzk project's architectural vision.