The Version That Wasn't: A Cargo Patch Pitfall in the cuzk Proving Engine
Introduction
In the course of building a pipelined SNARK proving daemon for Filecoin's Curio project, an assistant encountered a subtle but instructive failure mode in Rust's Cargo dependency management. The message at [msg 421] captures a moment of verification — a cargo check command intended to confirm that a freshly created bellperson fork integrates correctly with the workspace. Instead of a clean compilation, the command produced a warning: the patched dependency was not used in the crate graph. This article examines why that happened, what assumptions led to it, and what the episode reveals about the interplay between semver semantics, Cargo's patch mechanism, and the practical challenges of maintaining minimal forks in large Rust projects.
Context: The cuzk Phase 2 Pipeline
The cuzk project is a pipelined SNARK proving daemon designed to reduce the ~200 GiB peak memory footprint of Filecoin's Groth16 proof generation. Phase 1 had just been completed, delivering a working daemon with gRPC API, multi-GPU worker pool, and test data generation. Phase 2 aimed to split the proof generation pipeline into two stages — CPU-intensive circuit synthesis and GPU-intensive proving — so that intermediate state could be streamed rather than held entirely in memory.
The key architectural insight was that bellperson (the Rust Groth16 implementation used by Filecoin) already contained the necessary split internally. The function synthesize_circuits_batch() performed CPU synthesis, and the GPU-phase code lived in create_proof_batch_priority_inner. These were private. The plan was to create a minimal fork of bellperson — approximately 130 lines of changes — that would expose these internals as public API, allowing cuzk to call them independently.
The fork was created at extern/bellperson/ by copying the upstream source. Three modifications were applied: making ProvingAssignment and its fields public, exposing synthesize_circuits_batch(), and adding a new prove_from_assignments() function for the GPU phase. The fork's version was set to 0.26.0-cuzk.1 — a pre-release version based on upstream's 0.26.0.
The Subject Message: A Verification That Revealed a Problem
The assistant had just edited the workspace Cargo.toml to add a [patch.crates-io] section pointing to the fork. The natural next step was to verify compilation:
``[assistant] Now let's check if everything compiles: [bash] cargo check --workspace --no-default-features 2>&1 warning: Patchbellperson v0.26.0-cuzk.1 (/home/theuser/curio/extern/bellperson)was not used in the crate graph. Check that the patched package version and available features are compatible with the dependency requirements. If the patch has a different version from what is locked in the Cargo.lock file, runcargo updateto use the new version. This may also occur with an optional dependency that is not enabled. Finisheddevprofile [unoptimized + debuginfog] target(s) in 0.12s``
The warning is the entire story. Cargo is telling the assistant, in clear language, that the patch was not applied. The reason: the patched version (0.26.0-cuzk.1) does not satisfy the dependency requirement (bellperson = "0.26.0"). The compilation succeeded only because the original upstream version was still used from the registry cache — the fork was silently ignored.
The Reasoning Behind the Version Choice
Why did the assistant choose 0.26.0-cuzk.1 as the fork version? The reasoning was likely: "This is a fork of bellperson 0.26.0, so I'll use that as the base version and append a pre-release suffix to distinguish it." In Rust's semver, 0.26.0-cuzk.1 is a pre-release version of 0.26.0. Pre-release versions have lower precedence than the normal version — they are considered "older" in semver ordering.
The assistant's implicit assumption was that Cargo's [patch] mechanism would match the fork to any dependency requirement for bellperson, regardless of the exact version string. This assumption is understandable: the patch mechanism is explicitly designed to replace a dependency with a local override. Why wouldn't it work?
The Mistake: Pre-release Versions and Exact Version Matching
The mistake lies in a subtle interaction between two Cargo features. The [patch.crates-io] section works by intercepting dependency resolution: when Cargo resolves a dependency on bellperson = "0.26.0", it looks for a package whose version matches that requirement. The upstream bellperson 0.26.0 matches. The fork bellperson 0.26.0-cuzk.1 does not match, because in semver, a pre-release version (0.26.0-cuzk.1) is not considered to satisfy a requirement for the release version (0.26.0).
This is a deliberate design choice in semver. Pre-release versions are intended for testing and development — they signal instability. A dependency that requires 0.26.0 is asking for the stable release, not a pre-release of the same version. Cargo enforces this distinction even in the patch mechanism.
The assistant's error was treating the version as an arbitrary label rather than respecting semver semantics. The suffix -cuzk.1 was meant as a human-readable identifier, but Cargo interpreted it as a semver pre-release tag with specific meaning.
Detection and Recovery
The warning message is remarkably informative. It tells the assistant exactly what went wrong and provides the fix: "run cargo update to use the new version." However, the warning alone is not sufficient — cargo update would not fix a fundamental version mismatch. The real fix, applied in the next message ([msg 422]), was to change the fork's version back to 0.26.0 (removing the pre-release suffix). With the version matching exactly, cargo update -p bellperson then successfully locked the fork into the workspace.
The recovery was straightforward once the root cause was understood. But the episode could have been more costly. If the assistant had not run cargo check and instead proceeded to implement the pipelined prover against the fork's new API, the code would have compiled against the upstream bellperson (which lacks the public API changes), producing confusing "method not found" errors that would be difficult to trace back to the version mismatch.
Broader Lessons
This message illustrates several important principles for working with Rust's dependency system:
Semver is not arbitrary. Version strings carry semantic meaning in Cargo. A pre-release suffix changes how the version is matched. Using 0.26.0-cuzk.1 when you mean "a drop-in replacement for 0.26.0" is incorrect — you should use 0.26.0 or a higher version like 0.26.1-cuzk.1.
The patch mechanism requires version compatibility. [patch.crates-io] does not perform fuzzy matching. The patched version must satisfy the dependency requirements of all dependents. This is by design: Cargo needs to ensure that replacing a dependency doesn't break semver guarantees.
Verification is essential. The assistant's discipline of running cargo check immediately after making a change caught the problem at the earliest possible moment. Had the assistant assumed success and moved on, the error would have manifested later in a much more confusing form.
Cargo's warnings are actionable. The warning message in this case is exemplary: it states the problem, explains the likely cause, and suggests a fix. Reading Cargo warnings carefully — rather than treating them as noise — is a habit that pays dividends.
Input and Output Knowledge
To understand this message, a reader needs knowledge of: Rust's Cargo build system, the [patch.crates-io] mechanism for overriding dependencies, semver versioning conventions (including pre-release semantics), the bellperson library's role in Groth16 proof generation, and the broader cuzk project context (Phase 2 pipeline, the need to fork bellperson, the minimal-change philosophy).
The message creates knowledge about: the failure mode of version-mismatched patches in Cargo, the specific semver rule that pre-release versions don't satisfy release-version requirements, and the importance of exact version matching when using [patch.crates-io]. It also documents a debugging moment that shaped the subsequent implementation approach.
Conclusion
The message at [msg 421] is a small but instructive moment in a larger engineering effort. A single cargo check command revealed a subtle versioning mistake that could have derailed the Phase 2 implementation. The assistant's assumption — that a pre-release version suffix would be transparent to Cargo's patch mechanism — was reasonable but incorrect. The warning caught the error, the fix was applied in the next round, and the workspace compiled cleanly. The episode serves as a reminder that even experienced Rust developers can stumble on semver edge cases, and that Cargo's diagnostic messages, when read carefully, provide the exact information needed to recover.