The Dependency That Wasn't There: A Cargo Feature Resolution Failure in the cuzk Proving Engine

Introduction

In the middle of implementing Phase 2 of the cuzk pipelined SNARK proving engine—a sophisticated system designed to replace the monolithic PoRep C2 prover with a per-partition synthesis/GPU architecture—the assistant encountered a seemingly mundane but deeply instructive build failure. The message at <msg id=451> captures a single bash command and its error output: a cargo check invocation that fails because storage-proofs-porep does not have a cuda-supraseal feature, even though cuzk-core explicitly requests it. This short error message, spanning only a few lines, reveals a cascade of assumptions about Cargo's feature propagation model, the architecture of the dependency graph, and the boundary between compile-time and runtime code paths in a mixed CPU/GPU proving system.

The Message in Context

The message appears at a critical juncture in the Phase 2 implementation. The assistant had just completed Step 3—creating the srs_manager.rs module for direct SRS parameter loading via SuprasealParameters—and was in the middle of Step 4a: adding new dependencies to cuzk-core/Cargo.toml. The workspace Cargo.toml had been edited at <msg id=449> to add workspace-level dependency declarations for filecoin-proofs, storage-proofs-core, storage-proofs-porep, storage-proofs-post, storage-proofs-update, bellperson, blstrs, rayon, and ff. Then cuzk-core/Cargo.toml was edited at <msg id=450> to actually use these dependencies, with storage-proofs-porep specified as:

storage-proofs-porep = { workspace = true, features = ["cuda-supraseal"] }

The intent was clear: the pipeline module needs to call into storage-proofs-porep's circuit synthesis functions, and those functions must operate in the cuda-supraseal mode to produce GPU-compatible proofs. The assistant then ran cargo check --workspace --no-default-features to verify that the non-GPU compilation path still works—a standard sanity check before attempting a full GPU build. This is where the error surfaced.

Anatomy of the Error

The error message is a Cargo dependency resolution failure:

error: failed to select a version for `storage-proofs-porep`.
    ... required by package `cuzk-core v0.1.0`
versions that meet the requirements `^19.0` (locked to 19.0.1) are: 19.0.1

the package `cuzk-core` depends on `storage-proofs-porep`, with features: `cuda-supraseal` 
but `storage-proofs-porep` does not have these features.

Cargo's feature resolution is strict: when package A depends on package B with features = ["foo"], package B must actually define a Cargo feature named foo. The storage-proofs-porep crate, at version 19.0.1, does not define a cuda-supraseal feature. This feature exists on other crates in the dependency graph—it is defined on bellperson (where it gates the supraseal prover module and the SuprasealParameters type), and it is likely defined as a workspace-level feature in the Curio project's root Cargo.toml. But it does not exist on storage-proofs-porep.

The assistant's assumption was that feature names propagate transitively through the dependency graph: that requesting cuda-supraseal on storage-proofs-porep would somehow enable the feature on its downstream dependencies (like bellperson). This is not how Cargo works. Features are per-package namespaced identifiers. Package A can request feature foo on package B only if B explicitly declares [features] foo = [] in its own Cargo.toml. There is no implicit forwarding or inheritance.

Why This Assumption Was Made

To understand the mistake, we must examine the reasoning that led to it. The assistant had spent several messages investigating the full call chain from filecoin-proofs-api's seal_commit_phase2 down to the circuit construction. The research at <msg id=435> and <msg id=436> revealed that the proving path goes through storage-proofs-porep's synthesize_circuits_batch() and prove_from_assignments() functions, which in turn call into bellperson's Groth16 prover. The bellperson fork created at <msg id=434> exposes these functions under the cuda-supraseal feature gate.

The assistant's mental model was: "The pipeline needs cuda-supraseal mode throughout the entire call chain. I must ensure every crate in that chain has the feature enabled." This is a reasonable instinct—GPU proving requires the cuda-supraseal code path to be active at every layer. But the mechanism for achieving this is not to request the feature on each intermediate crate. Instead, the feature should be enabled at the top level (the workspace or the final binary), and each intermediate crate should use conditional compilation (#[cfg(feature = "cuda-supraseal")]) to check whether the feature is active on itself, not on its dependencies.

The storage-proofs-porep crate does not need its own cuda-supraseal feature. It uses bellperson internally, and bellperson's cuda-supraseal feature is enabled transitively when the top-level crate enables it. The storage-proofs-porep crate simply calls bellperson functions; it doesn't need to know whether those functions are GPU-accelerated or not. The feature gating happens inside bellperson, not in storage-proofs-porep.

Input Knowledge Required

To fully understand this message, a reader needs:

  1. Cargo's feature resolution model: Features are per-package. Package A can only enable feature foo on package B if B's Cargo.toml declares [features] foo = []. Features do not automatically propagate across dependency boundaries.
  2. The cuzk dependency graph: cuzk-corestorage-proofs-porepbellpersonsupraseal-c2. The cuda-supraseal feature is defined on bellperson and on the workspace root, but not on storage-proofs-porep.
  3. The --no-default-features flag: This tells Cargo to not activate default features for any package in the workspace. It is used to verify that code compiles in a minimal configuration, without GPU support.
  4. The Phase 2 architecture: The pipeline splits PoRep C2 proving into two phases—CPU circuit synthesis and GPU NTT+MSM—and needs both paths to be conditionally compiled based on GPU availability.
  5. The bellperson fork: A minimal fork was created at <msg id=434> to expose synthesize_circuits_batch() and prove_from_assignments() as public APIs, gated behind cuda-supraseal.

Output Knowledge Created

The error message itself is a form of output knowledge. It teaches us:

  1. storage-proofs-porep 19.0.1 does not define a cuda-supraseal feature. This is a concrete fact about the crate's interface.
  2. The feature-forwarding approach is incorrect. The correct approach is to either remove the features = ["cuda-supraseal"] from the storage-proofs-porep dependency (since the feature is not needed at that level) or to use a different mechanism for conditional compilation.
  3. The build system needs adjustment. The assistant will need to either: - Remove the feature request from storage-proofs-porep and rely on transitive feature propagation from the workspace level. - Or conditionally compile the entire pipeline module behind #[cfg(feature = "cuda-supraseal")] at the cuzk-core level, and only add the storage-proofs-porep dependency when the feature is active.
  4. The --no-default-features check caught the error early. This is a good practice—verifying compilation in a minimal configuration before attempting a full GPU build prevents confusing errors later.

The Broader Significance

This error, while small, illuminates a fundamental tension in the cuzk architecture. The proving engine must support two modes: a CPU-only fallback (Phase 1's monolithic approach) and a GPU-accelerated pipelined mode (Phase 2). The code must compile in both configurations, and the dependency graph must be correct for each. Cargo's feature system is the mechanism for this, but it requires careful discipline:

Conclusion

The message at <msg id=451> is a classic "fail fast" moment in software engineering. A simple cargo check caught a conceptual error before it could propagate into broken code. The error forced a reassessment of how cuda-supraseal should be managed across the crate graph, leading to a cleaner separation between the GPU and CPU code paths. In the broader narrative of the cuzk Phase 2 implementation, this moment represents the transition from design assumption to build-system reality—a reminder that even the most carefully researched architecture must survive contact with the compiler.