The Last Link in the Chain: Exporting the Phase 12 Split API Through the Rust Module Boundary

A Single Read, a World of Context

At first glance, message [msg 2893] appears deceptively simple. The assistant reads a file and observes:

Now I need to export the new types/functions from the bellperson groth16 module:

It then displays the contents of bellperson/src/groth16/mod.rs, showing the existing pub use re-exports that form the module's public API. There is no edit, no code change, no dramatic breakthrough. Yet this modest read operation represents a critical juncture in one of the most intricate optimization campaigns in the entire opencode session: the Phase 12 split API, designed to hide the latency of b_g2_msm — a CPU-bound multi-scalar multiplication that was keeping GPU workers idle and throttling throughput in the Filecoin PoRep proof generation pipeline.

To understand why this message matters, one must trace the path that led here: eleven phases of iterative optimization, each peeling back a deeper layer of bottleneck, from GPU kernel occupancy to DDR5 memory bandwidth contention to PCIe transfer patterns. Phase 12 is the culmination of a specific insight — that the GPU worker's critical path could be decoupled from CPU post-processing, allowing the worker to immediately pick up the next synthesis job while the CPU finishes computing b_g2_msm and assembling the final proof. And this message, a simple read of a module file, is the moment where that architectural change crosses the final boundary between implementation and integration.

The Architecture of the Split API

The Phase 12 split API is a study in cross-language systems design. At the C++ level, the monolithic generate_groth16_proofs function was split into two entry points: generate_groth16_proofs_start_c and finalize_groth16_proof. The first function performs all GPU work — kernel launches, multi-scalar multiplications on the device, and the critical GPU lock acquire/release — then packages the intermediate state into a heap-allocated groth16_pending_proof struct and returns an opaque handle. The second function takes that handle, joins the b_g2_msm thread (which was spawned during the start phase and may still be running), runs the epilogue (computing the final proof elements from the accumulated results), and frees the handle.

This split required a fundamental restructuring of the C++ code. The groth16_pending_proof struct had to be allocated early — before any threads were spawned — so that its fields (results, batch_add_res, split_vectors_l/a/b, tail_msm_*_bases, the split flags, and the b_g2_msm thread handle) served as the stable memory locations that all worker threads could reference. The assistant spent several messages ([msg 2864] through [msg 2878]) carefully refactoring the C++ to move local variables into the handle, fixing ordering issues where pp was referenced before allocation, and resolving a subtle mult_pippenger type mismatch caused by const/non-const pointer ambiguity in a ternary expression.

Once the C++ side compiled successfully ([msg 2886]), the assistant moved to the Rust FFI boundary. In supraseal-c2/src/lib.rs ([msg 2888]), new extern declarations were added for generate_groth16_proofs_start_c and finalize_groth16_proof, along with the opaque PendingProofHandle type. Then in bellperson/src/groth16/prover/supraseal.rs ([msg 2892]), Rust wrapper functions prove_start and prove_finish were created, along with a PendingProofHandle struct that wraps the raw C++ pointer and implements Drop to ensure proper cleanup.

Why This Read Matters

Message [msg 2893] sits at the precise moment when this entire chain of changes needs to be connected to the rest of the Rust codebase. The bellperson/src/groth16/mod.rs file is the module's public face — it re-exports selected symbols from submodules under the #[cfg(feature = "cuda-supraseal")] conditional compilation flag. Without updating this file, the new prove_start, prove_finish, and PendingProofHandle symbols exist in the supraseal submodule but are invisible to any code importing from bellperson::groth16.

The assistant's read operation reveals the current state of the exports:

#[cfg(feature = "cuda-supraseal")]
pub use self::prover::supraseal::{
    alloc_gpu_mutex, free_gpu_mutex, prove_from_assignments, synthesize_circuits_batch,
    synthesize_circuits_batch_with_hint, GpuMutexPtr, SendableGpuMutex, SynthesisCapacityHint,
};

This list includes the existing proving infrastructure — the monolithic prove_from_assignments, the synthesis functions, the GPU mutex types. The new split API symbols must be added to this list. The decision of how to add them is informed by the existing pattern: each symbol is listed by name, and the module follows a flat re-export style rather than re-exporting the entire submodule.

The Reasoning Behind the Export Strategy

The assistant's choice to read before editing reflects a disciplined engineering approach. In a cross-language project spanning C++, Rust FFI, and application-level orchestration, the module export file is a single point of truth for the public API surface. Reading it confirms:

  1. The exact syntax: The existing exports use pub use self::prover::supraseal::{...} with a brace-delimited list. New symbols must follow the same format.
  2. The conditional compilation guard: All exports are wrapped in #[cfg(feature = "cuda-supraseal")], ensuring the split API only exists when the CUDA backend is enabled. This is critical for maintaining a clean build when the feature is absent.
  3. The naming conventions: Existing symbols use snake_case for functions and PascalCase for types. PendingProofHandle and prove_start/prove_finish follow these conventions naturally.
  4. The absence of conflicts: No existing symbol clashes with the new names, so no renaming or disambiguation is needed. This read-before-edit pattern is characteristic of the assistant's methodology throughout the session. Rather than assuming the file's contents, the assistant verifies the current state, reducing the risk of edit conflicts or syntax errors. In a codebase where a single misplaced comma or missing conditional guard can break the build, this caution is well-founded.

The Broader Significance: Cross-Boundary Integration

The Phase 12 split API is an exercise in managing complexity across multiple abstraction boundaries. The C++ layer handles GPU kernel launches, memory management, and thread synchronization. The Rust FFI layer translates between C ABI conventions and Rust's ownership model, wrapping raw pointers in safe abstractions. The bellperson library provides the high-level proving API that application code (the Curio daemon's engine loop) calls. And the module exports in mod.rs are the final gate that determines what external consumers can actually use.

Each boundary introduces translation costs and failure modes. The C++/Rust boundary requires careful handling of opaque pointers, lifetime management, and error propagation. The internal/external module boundary in Rust requires explicit re-export decisions — a symbol not listed in pub use is effectively invisible, even if it's pub in its defining module. The assistant's systematic approach — implement C++, add FFI declarations, add Rust wrappers, update module exports — ensures that no boundary is left incomplete.

This message also reveals an implicit assumption: that the new symbols should be exported from the same module and with the same visibility as the existing proving functions. This is a reasonable design choice — the split API is a variant of the same proving operation, not a fundamentally new capability. However, it's worth noting that the assistant does not consider alternative export strategies, such as a separate submodule or a feature-gated re-export behind a different cfg flag. The existing pattern is followed without discussion, which is pragmatic for a codebase where consistency matters more than novelty.

The Input Knowledge Required

To fully understand this message, one must grasp several layers of context:

The Output Knowledge Created

While this message produces no code change itself, it creates knowledge in several dimensions:

  1. Documentation of the current state: The read output confirms exactly what the module currently exports, serving as a reference point for the edit that follows in [msg 2894].
  2. Validation of the integration plan: By confirming the file's structure, the assistant validates that the planned edit (adding three new symbols to the export list) is straightforward and follows existing conventions.
  3. A checkpoint in the implementation timeline: This message marks the transition from "implementing the Rust wrappers" to "integrating into the public API." The next message ([msg 2894]) applies the edit, and subsequent messages move into pipeline and engine integration.

The Thinking Process Revealed

The assistant's reasoning, while not explicit in this message, can be inferred from the sequence of actions. The phrase "Now I need to export the new types/functions" indicates a clear understanding of the dependency chain: the C++ changes are done, the FFI declarations are done, the Rust wrappers are done, and the next step is making them publicly available. The assistant does not ask whether this step is needed — it is assumed as a natural consequence of the module architecture.

The choice to read rather than directly edit suggests a risk-aware mindset. The assistant could have written the edit from memory, relying on knowledge of the file's structure from earlier reads. But the explicit read operation, occurring immediately before the edit, indicates a preference for verification over assumption. In a session where a single mis-typed character can trigger a multi-minute rebuild cycle, this caution is rational.

Conclusion

Message [msg 2893] is a quiet but essential moment in the Phase 12 implementation. It represents the point where an architectural change — the decoupling of GPU work from CPU post-processing — crosses from implementation into integration. The read operation is not merely a lookup; it is a deliberate act of verification, ensuring that the final link in the Rust-side API chain is correctly understood before it is modified. In the broader narrative of the optimization campaign, this message is the calm before the storm of engine-loop refactoring that follows, where the split API must be woven into the worker's critical path to deliver its promised throughput gains.