The Architecture of Integration: Building Phase 1 of the cuzk Proving Engine

Introduction

In the span of a single concentrated development session, the cuzk proving engine underwent a transformation that redefined its architectural scope. What began as a single-purpose PoRep (Proof-of-Replication) daemon—capable of generating one proof type on one GPU—emerged as a universal Filecoin proving service supporting all four proof types across multiple GPUs with priority-aware scheduling. This was Phase 1 of a multi-phase project to build a persistent, GPU-resident SNARK proving engine modeled after inference serving systems like vLLM, and its completion marked a critical inflection point in the project's trajectory.

The journey from the user's three-word instruction—"Proceed with phase 1"—to a working, compiled implementation with 8 passing unit tests was not a straight line. It was a recursive cycle of research, implementation, validation, and reflection that touched six files across four crates, required tracing enum discriminants across three programming languages (Go, C FFI, and Rust), and demanded architectural decisions that balanced immediate capability against long-term design goals. This article synthesizes the entire Phase 1 implementation, examining the research methodology, the architectural decisions, the hidden complexities discovered along the way, and the resulting system that emerged.

The Research Campaign: Mapping the Unknown

The Phase 1 implementation was preceded by one of the most thorough research campaigns in the project's history. The assistant systematically explored every layer of the system, starting with the cuzk workspace itself. In a single task invocation, the assistant read every source file in the extern/cuzk/ workspace—all five active crates, their Cargo.toml files, the engine, scheduler, prover, types, and service modules. This established a complete baseline understanding of what existed and what needed to change.

The research then extended outward to the external dependencies that Phase 1 would rely on. The filecoin-proofs-api crate (version 19.0.0) was examined in detail, documenting the exact Rust function signatures for generate_winning_post_with_vanilla, generate_single_window_post_with_vanilla, and generate_empty_sector_update_proof_with_vanilla. This was not a casual skim—the assistant searched through the registry source, examined type definitions, and documented the precise parameter types including ChallengeSeed, Commitment, ProverId, and the critical RegisteredSealProof enum values.

The investigation then moved to Curio's Go FFI layer, reading compute_do.go and local.go to understand how vanilla proofs are serialized for each proof type in production. This revealed a critical design constraint: PoSt proofs require multiple vanilla proofs per request (one per sector), unlike PoRep which uses a single vanilla proof. SnapDeals, meanwhile, needs three commitment CIDs rather than one. This discovery directly drove the protobuf changes that would follow, as the existing single-field schema could not accommodate the multi-proof nature of PoSt and SnapDeals requests.

The Data Model Foundation: Protobuf and Types

With the research complete, the assistant began implementation with the outermost contract of the system: the protobuf definition. The first code change of Phase 1 was an edit to proving.proto that changed the vanilla_proof field from a single bytes to repeated bytes vanilla_proofs. This single change enabled multi-proof support while maintaining backward compatibility with Phase 0 clients. The choice to use repeated bytes rather than an alternative encoding reflected a commitment to protobuf's native repeated-field semantics. It made the API self-describing, allowed protobuf libraries to handle parsing automatically, and aligned with gRPC best practices.

The decision to modify the existing field rather than creating a separate field for multi-proof requests revealed an important architectural assumption: that the protobuf schema should unify all proof types under a single request structure, with the proof_kind enum disambiguating which fields are relevant. This is a design philosophy that prioritizes simplicity and consistency over type-specific optimization. A single request type is easier to document, easier to route, and easier to extend than a family of specialized types.

Following the protobuf change, the assistant updated cuzk-core/src/types.rs to add the vanilla_proofs: Vec<Vec<u8>> field to the ProofRequest struct and rename SnapDeals commitment fields to match the API conventions discovered during research. These two changes—the protobuf schema and the core types—formed the data model foundation for everything that followed. Before you can implement proving functions, you must first define what data those functions receive.

The Enum Mapping: Tracing Values Across Three Language Boundaries

One of the most delicate challenges in Phase 1 was correctly mapping numeric proof-type identifiers across three language boundaries: Go (Curio's task orchestrator) → C FFI → Rust (filecoin-proofs-api). The gRPC protocol carries a registered_proof field as an integer, and this integer must be converted to the correct Rust enum variant before calling the proving functions. Getting this wrong would mean silently generating the wrong kind of proof—a bug that would produce an invalid proof, not a crash, making it extremely difficult to diagnose.

The assistant's investigation of this mapping spanned multiple messages and employed a variety of search strategies. The Go-side enum constants (StackedDrgWinning32GiBV1, StackedDrgWindow32GiBV1) were found in workflows.go and proofs_test.go, but the actual numeric values were defined as references to C macros, not explicit integers. The C headers were searched, but the enum definitions were not directly visible—they were generated by the Rust FFI's #[repr(i32)] attribute.

The breakthrough came when the assistant pivoted to the Rust FFI source. Reading filecoin-ffi/rust/src/proofs/types.rs revealed the #[repr(i32)] enum definition with auto-incrementing discriminants. The key insight was that the FFI enum uses simple declaration-order discriminants (0, 1, 2, ...), and critically, V1_1 in Go/FFI maps to V1_2 in filecoin-proofs-api. This naming discrepancy—where the same proof version is called different things in different layers—is exactly the kind of trap that a less thorough investigation would miss. The final mapping documented 15 variants of RegisteredPoStProof with values 0 through 14, covering WinningPoSt (values 0-4), WindowPoSt (values 5-9), and WindowPoSt V1_1 (values 10-14). This mapping became the Rosetta Stone that allowed the cuzk engine to interpret gRPC proof type identifiers correctly.

As [2] notes, "Every complex integration project has a Rosetta Stone—a single artifact that, once understood, unlocks the entire system. For the cuzk proving engine's Phase 1 expansion, that artifact was the #[repr(i32)] enum RegisteredUpdateProof in the filecoin-ffi Rust layer." The enum at the boundary was where integration began, and understanding it was the prerequisite for everything that followed.

The Proving Functions: From Research to Implementation

With the data model and enum mapping established, the assistant implemented the three new proving functions in prover.rs. Each function followed the pattern established by the existing PoRep C2 implementation but with the specific parameter signatures required by the filecoin-proofs-api.

The prove_winning_post() function calls generate_winning_post_with_vanilla() with the registered proof type, miner ID, randomness, vanilla proofs, and sector number. WinningPoSt is the most time-critical proof type in Filecoin—it must complete within a 30-second epoch window to avoid losing mining rewards. The implementation includes tracing spans with job_id correlation for log debugging, timing instrumentation (deserialization, proving, total), and proper error handling.

The prove_window_post() function calls generate_single_window_post_with_vanilla() with similar parameters plus a partition index. WindowPoSt is less time-critical than WinningPoSt but still subject to deadlines—provers must submit proofs for all sectors within a 24-hour proving period. The implementation includes the critical V1_1 to V1_2 mapping, ensuring that the grindability fix in the WindowPoSt circuit is applied correctly.

The prove_snap_deals() function calls generate_empty_sector_update_proof_with_vanilla() with three commitment CIDs (sector_key_cid, new_sealed_cid, new_unsealed_cid) and vanilla proofs. SnapDeals is the most recently added proof type, enabling sector updates without re-sealing the entire sector. The three-commitment structure reflects the SnapDeals protocol's requirement to prove that the sector was updated correctly.

Each function maintains backward compatibility with the existing PoRep C2 path, ensuring that Phase 0 clients continue to work without modification. The implementation also includes proper error handling for unsupported proof types, returning a clear error message rather than silently failing.

The Multi-GPU Worker Pool: Architectural Centerpiece

The engine refactoring for multi-GPU support was the most architecturally significant change in Phase 1. The assistant implemented a worker pool with automatic GPU detection via nvidia-smi, spawning one worker thread per GPU. Each worker sets CUDA_VISIBLE_DEVICES=<ordinal> for device isolation, ensuring that CUDA API calls target the correct GPU.

The design made several deliberate choices:

One worker per GPU: Rather than a dynamic pool where workers claim GPUs as needed, the engine spawns one worker per GPU at startup and maintains that mapping for the daemon's lifetime. This avoids the overhead of CUDA context initialization per proof and aligns with the SRS residency strategy—keeping parameters loaded in GPU memory across proof requests requires dedicated GPU ownership.

Process-level GPU isolation via CUDA_VISIBLE_DEVICES: Each worker sets this environment variable before making any CUDA calls, ensuring it can only see its assigned GPU. This is a stronger isolation mechanism than CUDA streams or contexts within a single process—a crash in one worker doesn't affect others, and resource accounting is simpler. The trade-off is higher memory overhead (each worker loads its own CUDA runtime) and more complex inter-worker communication.

Shared priority queue with BinaryHeap: All workers compete for jobs from a single priority queue, with the scheduler using a BinaryHeap to maintain priority ordering (CRITICAL > HIGH > NORMAL > LOW). This is a classic work-stealing pattern that maximizes throughput—all workers stay busy as long as there are N+ jobs pending—and ensures that the highest-priority job is always served next regardless of which worker picks it up.

The scheduler was rewritten with four priority levels: CRITICAL (for WinningPoSt, which must complete within a 30-second epoch window), HIGH (for WindowPoSt), NORMAL (for PoRep and SnapDeals), and LOW (for background/testing). The shared priority queue ensures that higher-priority jobs are always dequeued before lower-priority jobs, regardless of submission order. This is critical for WinningPoSt, where a delay of even a few seconds can mean losing a block reward.

The Deferred Decision: GPU Affinity and Architectural Honesty

One of the most revealing moments in Phase 1 was the post-commit review, where the assistant considered whether to implement GPU affinity-based scheduling and concluded it should be deferred to Phase 2. The reasoning was precise and architecturally honest: the GROTH_PARAM_MEMORY_CACHE is a process-global lazy_static HashMap, meaning the SRS lives in host memory, not in per-GPU VRAM. All workers benefit from the same cache regardless of which GPU they control. True GPU affinity—routing jobs based on which GPU has the SRS in VRAM—requires the custom SRS manager planned for Phase 2. In Phase 1, the shared queue is not just simpler; it is architecturally correct.

This decision exemplifies a pattern that recurs throughout the project: the willingness to defer a feature not because it is difficult to implement, but because the underlying infrastructure doesn't yet support it meaningfully. The assistant could have implemented GPU affinity scheduling in Phase 1—it would have been straightforward to add a HashMap<GpuId, Vec<JobId>> to track which GPU processed which job. But without per-GPU SRS residency tracking, that affinity would have been cosmetic. The scheduler would route jobs to the "right" GPU without any guarantee that the SRS was actually loaded there.

As [1] notes, "The assistant did not guess at enum values—it traced them across three language boundaries. It did not assume GPU affinity was needed—it evaluated the actual SRS caching model and deferred the feature." This architectural honesty—building what the system needs now, not what the plan says it might need later—is the mark of engineering maturity.

The Type Import Investigation: Defensive Engineering in Practice

One of the most instructive sub-investigations in Phase 1 was the assistant's search for the EmptySectorUpdateProof and PartitionSnarkProof type imports. The grep command searching for these two type names in the filecoin-proofs-api crate's lib.rs returned a single match: PartitionSnarkProof was re-exported through the public API, while EmptySectorUpdateProof was not.

This single finding had immediate architectural consequences. For the WindowPoSt implementation, the assistant could import PartitionSnarkProof cleanly from the top-level filecoin_proofs_api crate. But for the SnapDeals implementation, it had to reach into filecoin_proofs_v1::types::EmptySectorUpdateProof—a deeper, less convenient import path that is more likely to break if the internal crate restructures its modules.

The assistant did not treat the grep result as definitive. It followed up by reading the update.rs module to confirm that EmptySectorUpdateProof was imported there from filecoin_proofs_v1::types, and read the full lib.rs to verify the complete re-export list. This pattern—grep first, then verify by reading the actual source—is a hallmark of rigorous research-driven development. It ensures that assumptions about API surfaces are validated against the actual code, not against documentation that may be incomplete or outdated.

The Bench Tool: Exercising All Proof Types

The Phase 1 implementation extended the cuzk-bench tool with full support for all proof types. The bench tool gained a --type flag supporting porep, winning-post, window-post, and snap proof types, along with --vanilla for PoSt/SnapDeals vanilla proof files, --partition for WindowPoSt partition index, and --comm-r/--comm-d flags for SnapDeals commitment CIDs. These extensions enabled operators to benchmark and validate all proof types through a single command-line interface, replacing the previous PoRep-only testing capability.

The bench tool changes were not merely cosmetic—they required proper serialization of multi-proof requests through the updated protobuf schema, correct mapping of proof type identifiers to the registered_proof enum values, and integration with the engine's priority-based dispatching. The tool was refactored into a ProofParams struct for clean batch, single, and concurrent modes, providing a foundation for the benchmarking infrastructure that will be needed in later phases.

Validation and Commit

The assistant validated the Phase 1 implementation through a deliberate verification sequence. First, cargo check --workspace --no-default-features confirmed that the entire workspace compiled without errors or warnings. Then, cargo test --workspace ran the existing test suite, which passed 5 tests. New enum conversion tests were added to validate the critical FFI-to-proofs-api mapping logic. A final test run confirmed all 8 tests passed, including the new tests.

The milestone was committed as d8aa4f1d with a detailed commit message enumerating all changes: prover implementations for all four proof types, multi-GPU worker pool with priority scheduling, protobuf API updates with repeated bytes vanilla_proofs, gRPC service updates for the new fields, and bench tool support for all proof types.

The Post-Commit Review: Assessing Completeness

After committing Phase 1, the assistant paused to review the project plan (cuzk-project.md) and assess remaining deliverables. This post-commit review revealed several important insights that shaped the path forward.

First, GPU affinity-based scheduling was formally deferred to Phase 2. The process-global GROTH_PARAM_MEMORY_CACHE makes per-GPU SRS tracking unnecessary in Phase 1—all workers share the same SRS cache, so there is no benefit to routing jobs to specific GPUs based on loaded SRS. This decision simplified the Phase 1 scheduler and avoided unnecessary complexity.

Second, the SRS warm tier was deferred to Phase 2. The existing GROTH_PARAM_MEMORY_CACHE provides sufficient SRS management for Phase 1. A custom tiered manager will be implemented in Phase 2 when per-worker SRS tracking becomes meaningful.

Third, the gen-vanilla command was identified as the next critical deliverable. Without vanilla proof test data, the new proving functions for WinningPoSt, WindowPoSt, and SnapDeals cannot be exercised with real proofs. The assistant researched the filecoin-proofs-api function signatures for vanilla proof generation and prepared for implementation.

Fourth, the batch command was already implemented in Phase 0 hardening. The Phase 1 extensions to the bench tool added support for all proof types to the existing batch command, completing the benchmarking infrastructure.

The assistant concluded that Phase 1 was substantially complete, with only the gen-vanilla command remaining as a critical dependency for end-to-end testing. This systematic review—comparing implemented deliverables against the documented plan—ensured that no gaps were overlooked and that the project maintained alignment with its architectural roadmap.

The Architecture of Systematic Engineering

Looking across the entire Phase 1 implementation, a clear pattern emerges. The assistant's methodology can be characterized as a sequence of concentric investigations, each narrowing the focus while deepening the understanding:

  1. Workspace reconnaissance: Read every source file in the cuzk workspace to establish baseline knowledge.
  2. API surface exploration: Examine the filecoin-proofs-api crate to document function signatures and type definitions.
  3. Serialization format research: Study Curio's Go FFI layer to understand how vanilla proofs are serialized in production.
  4. Protobuf and service analysis: Read the existing protobuf definition and gRPC service implementation to plan changes.
  5. Enum mapping investigation: Trace proof-type identifiers across Go, C FFI, and Rust layers to ensure correct dispatch.
  6. Type import verification: Verify the public API surface of external dependencies to ensure correct import paths. Each investigation produced actionable knowledge that directly informed the implementation. The protobuf change was informed by the discovery that PoSt proofs require multiple vanilla proofs. The enum mapping was informed by tracing values through three language boundaries. The type imports were informed by verifying which types were publicly re-exported. This methodology is not unique to AI-assisted development—it is the same systematic approach that experienced systems engineers use when integrating with complex, multi-language codebases. What is remarkable is the assistant's ability to execute this methodology autonomously, following the research thread wherever it leads, without needing to be told what to investigate next.

Looking Forward: From Phase 1 to Phase 2

Phase 1 established the foundation for the higher-impact optimizations in Phases 2-5. The multi-GPU worker pool structure is the foundation for the split synthesis/prove pipeline in Phase 2. The multi-proof protobuf extension (repeated bytes vanilla_proofs) is the foundation for cross-sector batching in Phase 3. The timing instrumentation provides the measurement infrastructure needed to evaluate compute optimizations in Phase 4. And the understanding of proof type characteristics gained in Phase 1 directly informs the Pre-Compiled Constraint Evaluator design in Phase 5.

The gen-vanilla command, identified as the next critical deliverable, will enable end-to-end testing of all proof types. By wrapping the filecoin-proofs-api vanilla generation functions directly (rather than wrapping lotus-bench), the bench tool will be able to generate test data for any proof type without external dependencies. This will complete the testing infrastructure and enable comprehensive validation of the Phase 1 implementation.

The research into vanilla proof generation APIs has already been completed. The assistant discovered the exact function signatures for generate_winning_post_sector_challenge, generate_fallback_sector_challenges, generate_single_vanilla_proof, and generate_partition_proofs, along with the type constructors for PrivateReplicaInfo, SectorId, and ProverId. This research set the stage for the next implementation push, ensuring that the gen-vanilla command can be built with the same research-driven confidence that characterized Phase 1.

Conclusion

Phase 1 of the cuzk proving engine transformed a PoRep-only daemon into a universal Filecoin proving service supporting all four proof types across multiple GPUs with priority-aware scheduling. The implementation touched six files across four crates, required tracing enum values across three language boundaries, and involved one of the most thorough research campaigns in the project's history.

The success of Phase 1 was not accidental. It was the product of a systematic methodology: research before implementation, understanding before code. The assistant read every source file in the workspace, examined every API signature, traced every serialization format, and verified every enum mapping before writing a single line of proving code. This discipline—the willingness to investigate rather than assume—is what separates robust systems engineering from fragile code construction. In a system that generates cryptographic proofs worth real economic value on the Filecoin network, this discipline is not a luxury—it is a necessity.

The architecture that emerged from Phase 1 is honest. It builds what the system needs now, not what the plan says it might need later. GPU affinity was deferred because the SRS caching model didn't support it. The shared priority queue was retained because it was architecturally correct. The gen-vanilla command was prioritized because it was the critical dependency for testing. These decisions reflect an engineering maturity that prioritizes correctness and testability over premature optimization.

The daemon that once could only prove PoRep C2 on a single GPU now handles all four Filecoin proof types across multiple GPUs with priority-aware scheduling. The architecture is sound, the implementation is validated, and the foundation for future optimizations is laid. The journey from "Proceed with phase 1" to a working, compiled, multi-proof proving engine spanned dozens of messages, hundreds of grep searches, and thousands of lines of code—but the result is a system that is greater than the sum of its parts.

References

[1] "From Research to Reality: How Phase 1 of the cuzk Proving Engine Was Built" — Chunk 0 analysis covering the comprehensive research campaign, protobuf changes, enum mapping investigation, multi-GPU architecture, and post-commit review.

[2] "From Enum to Engine: The Architecture of Phase 1 in the cuzk Proving Engine" — Chunk 1 analysis examining the enum mapping as the Rosetta Stone of integration, the moment of synthesis from research to implementation, multi-GPU design decisions, and the deferred GPU affinity decision.