The Art of the Accessible Interface: Making Prover Functions Public in cuzk's Phase 2 Pipeline

Message in Context

[assistant] [edit] /home/theuser/curio/extern/cuzk/cuzk-core/src/prover.rs
Edit applied successfully.

This terse confirmation message — the third in a sequence of three edits to cuzk-core/src/prover.rs — appears at first glance to be one of the most mundane moments in a coding session. A file was edited. The edit applied successfully. No errors, no drama. Yet this message sits at a critical juncture in the development of the cuzk proving engine's Phase 2 pipeline architecture, and it represents a deliberate architectural decision about module boundaries, API visibility, and the tension between encapsulation and composability.

The Context: A Performance Crisis Demands Architectural Change

To understand why this message matters, we must step back into the events that immediately preceded it. The assistant had just completed an end-to-end GPU test of the pipelined PoRep C2 proving path (<msg id=545-548>). The test was a qualified success: the proof was valid (1920 bytes, matching the expected 10 partitions × 192 bytes each), confirming that the bellperson fork, SRS manager, and per-partition synthesis/GPU pipeline all functioned correctly on real GPU hardware. But the performance numbers told a sobering story.

The sequential per-partition pipeline — synthesize partition 0, GPU-prove partition 0, synthesize partition 1, GPU-prove partition 1, and so on through all 10 partitions — took approximately 611 seconds total. The monolithic Phase 1 baseline, by contrast, completed the same work in roughly 93 seconds. This was a 6.6× performance regression, and it threatened to undermine the entire rationale for the Phase 2 pipeline architecture.

The root cause was clear: the monolithic seal_commit_phase2() function from filecoin-proofs-api synthesized all 10 partitions in parallel using rayon (completing synthesis in ~55 seconds total, not 10 × 55 seconds), then proved them all in a single GPU call (~35-40 seconds). The per-partition pipeline serialized this work, incurring the overhead of 10 separate synthesis calls and 10 separate GPU invocations.

The assistant correctly diagnosed that per-partition pipelining was designed for throughput on a stream of proofs — overlapping synthesis of proof N+1 with GPU proving of proof N — not for single-proof latency. But the immediate priority was to restore single-proof performance while preserving the pipeline architecture for future throughput gains. The solution: a batch-all-partitions synthesis mode that would synthesize all partitions in one rayon parallel call and prove them in one GPU call, matching the monolithic approach while still using the split synthesis/GPU pipeline internally.

The Rewrite: pipeline.rs Gets a Major Overhaul

The assistant's response was swift and methodical. It launched two subagent tasks to research the circuit construction APIs for PoSt and SnapDeals proofs (<msg id=555-556>), gathering the precise function signatures and parameter types needed. It then read the current pipeline.rs, engine.rs, and prover.rs files to plan the changes ([msg 554]). It added bincode as a direct dependency for deserializing PoSt vanilla proofs (<msg id=561-562>). And then, in message 564, it performed a complete rewrite of pipeline.rs — the central file of the Phase 2 architecture.

The new pipeline.rs added three major functions:

  1. synthesize_porep_c2_batch() — synthesizes all 10 partitions in a single rayon parallel call, then proves them in one GPU call, matching the monolithic approach
  2. synthesize_post() — handles both WinningPoSt and WindowPoSt synthesis
  3. synthesize_snap_deals() — handles SnapDeals synthesis The PoSt and SnapDeals synthesis functions required inlining vanilla proof partitioning logic from filecoin-proofs because the relevant modules were private and inaccessible from outside the crate — a common frustration when working with Rust's visibility system.

The Subject Message: Making the Connection

This brings us to the subject message and its two predecessors (<msg id=565-568>). The new synthesis functions in pipeline.rs needed to call prover functions — specifically, they needed access to the GPU proving functions that were previously only called internally within the prover module. In the Phase 1 architecture, the Engine was the sole consumer of prover functions, and everything was neatly encapsulated. But Phase 2 introduced a new consumer: the pipeline module, which needed to call prover functions directly rather than going through the engine's dispatch logic.

The assistant made three edits to prover.rs:

The Significance: API Visibility as Architectural Decision

In Rust, the pub keyword is a deceptively simple mechanism that encodes profound architectural decisions. Making a function public is not merely a technical necessity — it is a statement about module boundaries, about who is allowed to depend on what, and about the maintainer's intent for future evolution.

The assistant's decision to make prover functions public represents a deliberate shift in the module's contract. Previously, prover.rs was an internal implementation detail of the engine — a thin wrapper around filecoin-proofs-api calls that the engine invoked on behalf of external consumers. Now, it becomes a semi-public API that both the engine and the pipeline module depend on. This creates a new dependency relationship: pipelineproverfilecoin-proofs-api, alongside the existing engineproverfilecoin-proofs-api path.

This dual-consumer pattern has implications:

  1. Testing surface: With two consumers, changes to prover function signatures must be coordinated across both the engine and the pipeline, increasing the testing burden.
  2. Refactoring risk: If the prover module's internal logic changes (e.g., to support a new GPU backend), both consumers must be updated.
  3. Documentation burden: Public functions need documentation, especially when they are part of an internal crate that may eventually be used by external consumers via the planned cuzk-ffi C ABI. The assistant implicitly accepted these costs because the alternative — routing all pipeline calls through the engine — would have introduced unnecessary indirection and coupling. The pipeline module is a peer of the engine, not a subordinate, and direct access to prover functions is architecturally cleaner than forcing pipeline to call engine methods that merely delegate to prover.

Assumptions and Tradeoffs

The assistant made several assumptions in this sequence:

  1. That the prover functions would remain stable: The pipeline code was written against the current prover API. If the prover module is later refactored (e.g., to support async GPU submission or multi-device dispatch), the pipeline code will need corresponding updates.
  2. That direct access is preferable to indirection: Rather than adding methods to the Engine type that pipeline could call (which would preserve encapsulation), the assistant chose to make prover functions directly accessible. This is a reasonable choice for a performance-sensitive system where every layer of abstraction adds overhead, but it does weaken module boundaries.
  3. That the visibility changes are complete: The assistant made three edits, suggesting an iterative process of discovering which functions needed to be public. This implies that the initial compilation attempt revealed missing visibility, and the assistant progressively opened up the API until compilation succeeded.

Input and Output Knowledge

To understand this message, the reader needs:

The Thinking Process

The assistant's reasoning, visible across the sequence of messages, follows a clear pattern:

  1. Diagnose: The E2E test reveals a 6.6× performance regression. The root cause is identified as sequential per-partition processing.
  2. Plan: The solution is to add a batch-all-partitions mode that matches the monolithic approach. Additionally, PoSt and SnapDeals synthesis must be implemented.
  3. Research: Two subagent tasks gather the precise APIs needed for PoSt and SnapDeals circuit construction, including setup parameters and vanilla proof partitioning.
  4. Implement: The major pipeline.rs rewrite is completed in one large write operation.
  5. Connect: The pipeline code needs prover functions. Three edits to prover.rs make the necessary functions public.
  6. Verify: The next message (569) runs cargo check to verify compilation, revealing unused imports and warnings that need cleanup. The subject message is step 5 in this chain — the critical connection point between the new pipeline code and the existing prover infrastructure. Without these visibility changes, the pipeline rewrite would be dead code, unable to call the GPU proving functions it depends on.

Conclusion

A single line — "[edit] /home/theuser/curio/extern/cuzk/cuzk-core/src/prover.rs / Edit applied successfully." — belies the architectural significance of the change it records. In making prover functions public, the assistant reshaped the dependency graph of the cuzk proving engine, transforming prover.rs from an engine-internal implementation detail into a shared API consumed by both the engine and the pipeline module. This is the kind of invisible infrastructure work that makes or breaks a software architecture: the careful management of module boundaries, the deliberate choice of what to expose and what to hide, and the acceptance of long-term maintenance costs in exchange for immediate architectural clarity. The edit applied successfully, but its implications will echo through every future refactoring of the cuzk codebase.