The RegisteredProof That Wasn't Used: Tracing a Cross-Language Serialization Bug in CuZK
Introduction
In the intricate world of zero-knowledge proving systems, the difference between a working pipeline and a broken one can be vanishingly small — a single field skipped during serialization, a type mismatch between languages, or a redundant verification check that masks the real problem. Message [msg 1637] captures a pivotal moment in a deep debugging session where an AI assistant investigates a production bug in the Curio storage proving system: PoRep (Proof of Replication) tasks processed through the PSProve (Proof Sharing) pathway fail with "porep failed to validate" when using the CuZK GPU proving engine, even though the same PSProve pathway works without CuZK, and even though CuZK works fine for normal PoRep C2 proofs.
This message is a turning point in the investigation. The assistant has just made a critical observation about how the Rust CuZK prover handles the registered_proof field — an observation that eliminates one class of potential root causes and narrows the search to the precise plumbing difference between the two code paths. Let us examine this message in detail, unpacking the reasoning, the assumptions, the knowledge required, and the significance of what was discovered.
The Message in Full
The assistant writes:
I notice something crucial:prove_porep_c2takesvanilla_proof_json,sector_number, andminer_idas inputs. It deserializes the C1 wrapper, decodes base64, then deserializesSealCommitPhase1Output. Then at line 184:
>
``rust let output = seal::seal_commit_phase2(c1_output, prover_id, sector_id) ``
>
Thec1_outputcontainsregistered_proofINSIDE the SealCommitPhase1Output. Theregistered_prooffrom the gRPC request field is NOT used by the Rust prover for PoRep. The Rust prover uses theregistered_proofembedded in the C1 JSON.
>
So the question becomes: does the Go JSON round-trip preserve the registered_proof field correctly? Let me check:
>
In Rust JSON,registered_proofis serialized as the enum variant string, e.g.,"StackedDrg32GiBV1_1".
>
In Go'sCommit1OutRaw: ``go RegisteredProof StringRegisteredProofTypejson:"registered_proof"``
>
This is astringtype. It should round-trip perfectly: Rust outputs"StackedDrg32GiBV1_1", Go stores it as a string, Go re-marshals it as"StackedDrg32GiBV1_1". This is fine.
>
Now let me focus on theVanillaProofsmap key. In Rust JSON: ``json "vanilla_proofs": {"StackedDrg32GiBV1": [[...], ...]}``
>
Note: the map key is"StackedDrg32GiBV1"(without_1!), whileregistered_proofis"StackedDrg32GiBV1_1"(with_1). In Go: ``go VanillaProofs map[StringRegisteredProofType][][]VanillaStackedProofjson:"vanilla_proofs"``
>
The map key is a string. It round-trips fine.
>
OK wait, I should actually look at what happens in the non-cuzk path for PSProve, since the user said that works. The non-cuzk path at task_prove.go:316-317:
>
``go snarkProof, err = ffiselect.FFISelect.SealCommitPhase2(ctx, vproof, sectorID.Number, sectorID.Miner) ``
>
Wherevproofis the Go-re-marshaled JSON. This calls the FFI'sSealCommitPhase2which passes the JSON to Rust'sseal_commit_phase2. If the JSON round-trip were broken, this would also fail. But the user says PSProve without cuzk works fine.
>
So the JSON round-trip is fine. The issue must be in how the data is packaged for cuzk specifically.
>
Let me look at the C1OutputWrapper very carefully: [grep] C1OutputWrapper|c1OutputWrapper|C1_OUTPUT|phase1_out|Phase1Out Found 58 matches /tmp/czk/extern/cuzk/cuzk-core/src/pipeline.rs: Line 29: use crate::prover::C1OutputWrapper; Line 136: use filecoin_proofs_api::seal::SealCommitPhase1Output; Line 496: let wrapper: C1OutputWrapper = serde_json::from_slice(vanilla_proof_json) Line 500: &wrapper.phase1_out, Line 502: .context("failed to decode base64 Phase1Output")?; Line 503: let c1_output: SealCommitPhase1Output = serde_json::from_slice(&phase1_json_bytes) Line 504: .con...## Context: The PSProve PoRep Puzzle
To understand the significance of message [msg 1637], we must first understand the problem landscape. The Curio storage proving system has two distinct pathways for generating PoRep (Proof of Replication) SNARKs. The "normal" path runs on a single machine: the miner's node calls SealCommitPhase1 to produce a C1 output (the "vanilla proof"), then feeds those raw bytes directly into SealCommitPhase2 (or the CuZK GPU proving engine) to produce the final SNARK proof. The PSProve (Proof Sharing) path, by contrast, is designed for a marketplace model: a client machine generates the C1 output, uploads it to a central market database as JSON, and a separate provider machine (potentially with different hardware) fetches the C1 data and runs Phase 2 to produce the SNARK. This decoupling allows compute specialization — clients with cheap CPU can do Phase 1, while providers with expensive GPUs do Phase 2.
The bug manifests specifically at the intersection of PSProve and CuZK. PSProve PoRep works fine when using the standard CPU-based SealCommitPhase2 FFI call. CuZK PoRep C2 works fine in the normal (non-PSProve) path. But when PSProve sends its C1 data to CuZK, the resulting SNARK proof fails validation with "porep failed to validate". This is a narrow, specific failure mode that immediately suggests the problem lies in how PSProve packages data for CuZK — not in CuZK itself, and not in the PSProve data format generally.
The user's guidance was precise: compare the two code paths, look for differences in how the C1 output is wrapped, and trace the RegisteredProof enum mappings through the FFI, the CuZK protobuf, and the Rust code. The assistant had been pursuing several hypotheses — JSON round-trip corruption, parameter file mismatches, sector ID inconsistencies — but message [msg 1637] represents the moment when the investigation sharpens from broad speculation to focused structural comparison.
The Crucial Observation: RegisteredProof Is Ignored
The key insight in this message is the discovery that the Rust CuZK prover does not use the registered_proof field from the gRPC SubmitProofRequest. Instead, it reads the registered_proof value embedded inside the SealCommitPhase1Output JSON — that is, inside the C1 data itself. This is a critical architectural detail.
Why does this matter? In the normal CuZK path (cuzk_funcs.go), both the gRPC RegisteredProof field and the C1 output's internal registered_proof are set from the same source — the sector's ProofType enum. They are necessarily consistent. But in the PSProve path, the C1 output was generated on a different machine and transmitted via the market as JSON. The gRPC RegisteredProof field is set by the provider based on spt (derived from the C1 output's registered_proof string via ToABI()). If there were a mismatch between these two values — say, if ToABI() produced a different enum value than what the C1 output contained — it would be irrelevant for CuZK because CuZK ignores the gRPC field and uses the C1-internal value anyway.
This observation elegantly eliminates an entire class of potential bugs. The assistant does not need to trace enum mappings through protobuf definitions, check for off-by-one errors in the ABI conversion, or verify that the CuZK Rust code correctly interprets the RegisteredProof field. The field is simply not used for PoRep proofs. The only thing that matters is whether the C1 JSON round-trips correctly through Go serialization.
The Round-Trip Deduction
Having established that CuZK uses only the C1-internal registered_proof, the assistant turns to the JSON round-trip question. The PSProve path deserializes the C1 output from the market's JSON into a Go Commit1OutRaw struct, then re-marshals it to JSON before sending it to CuZK. If this round-trip altered the registered_proof field — say, by changing the string value or dropping it entirely — CuZK would use a wrong or missing proof type, and the SNARK would fail validation.
The assistant checks the Go struct definition: RegisteredProof is typed as StringRegisteredProofType, which is a Go string. The Rust JSON serializes the enum as a string like "StackedDrg32GiBV1_1". A Go string round-trips this value perfectly — no transformation, no truncation, no enum-to-integer conversion. The field is safe.
Then the assistant considers the VanillaProofs map. In Rust JSON, the map key is "StackedDrg32GiBV1" (without the _1 suffix), while registered_proof is "StackedDrg32GiBV1_1" (with _1). This is a known quirk of the Filecoin proof system: the vanilla proof map uses a slightly different key than the registered proof type. But again, both are strings, and Go's map[string]... round-trips them faithfully.
At this point, the assistant has proven that the JSON round-trip is correct for the fields that matter. But there is a subtle logical leap here: the assistant invokes the non-CuZK PSProve path as evidence. If the Go-re-marshaled JSON were corrupt, the non-CuZK path (ffiselect.FFISelect.SealCommitPhase2) would also fail, because it passes the same JSON to Rust's seal_commit_phase2. Since the user confirmed that PSProve without CuZK works fine, the JSON must be valid. This is a powerful cross-check: the data format is correct; the issue is in how CuZK receives it.
The Shift to the C1OutputWrapper
Having eliminated the JSON round-trip and the registered_proof mapping as root causes, the assistant pivots to the remaining structural difference: the C1OutputWrapper. In the normal CuZK path, the wrapper is constructed by the shared wrapC1Output function in cuzk_funcs.go. In the PSProve path, a local c1OutputWrapper struct is defined and populated within task_prove.go itself. These are not identical — the PSProve version uses int64 for SectorNum while the normal version may differ, and more importantly, the wrapper's Phase1Out field contains different data: in the normal path it holds raw Rust JSON bytes (the direct output of SealCommitPhase1), while in the PSProve path it holds Go-re-marshaled JSON bytes.
The assistant begins a grep for C1OutputWrapper to trace how the Rust CuZK engine deserializes this wrapper. The Rust code at pipeline.rs:496-504 parses the wrapper JSON, base64-decodes the phase1_out field, then deserializes the result as SealCommitPhase1Output. If the Go-re-marshaled JSON inside Phase1Out differs structurally from the original Rust JSON — even in ways that don't affect semantic correctness — the Rust serde_json deserializer might produce a different SealCommitPhase1Output struct. This is the frontier of the investigation.
Assumptions and Knowledge
This message rests on several assumptions and requires significant domain knowledge to follow. The assistant assumes that the Rust CuZK prover's behavior at line 184 is representative of all PoRep proof types — that registered_proof is always read from the C1 output, never from the gRPC request. This is a reasonable inference from reading the code, but it would need to be verified for edge cases like SnapDeals or WinningPoRep.
The assistant also assumes that the non-CuZK PSProve path is a perfect control experiment: if the JSON round-trip were broken, both CuZK and non-CuZK would fail. This is valid only if the non-CuZK path passes the exact same JSON bytes to Rust. The code at task_prove.go:316-317 shows that vproof (the re-marshaled JSON) is passed directly to SealCommitPhase2, so the assumption holds.
The knowledge required to understand this message is substantial: familiarity with the Filecoin proof pipeline (Phase 1/Phase 2 separation, vanilla proofs vs. SNARK proofs), understanding of Go-Rust FFI boundaries, knowledge of serde_json deserialization semantics, and awareness of the PSProve marketplace architecture. The assistant demonstrates a firm grasp of all these concepts.
Output Knowledge and Significance
This message creates critical output knowledge: the registered_proof field in the gRPC request is irrelevant for PoRep proofs in CuZK, the JSON round-trip is correct, and the investigation must focus on the C1OutputWrapper packaging. This narrows the search space from a dozen potential causes to a single, testable hypothesis: the Go-re-marshaled JSON inside the wrapper's Phase1Out field differs from the original Rust JSON in a way that affects Rust's serde_json deserialization of SealCommitPhase1Output.
The significance of this narrowing cannot be overstated. Debugging cross-language serialization issues is notoriously difficult because the failure manifests as a cryptographic validation error far downstream from the actual cause. By systematically eliminating the registered_proof mapping and the round-trip integrity, the assistant has isolated the problem to the wrapper layer — the boundary between Go's JSON marshaling and Rust's JSON deserialization. The next steps would involve comparing the raw bytes of the original Rust C1 JSON against the Go-re-marshaled version, looking for differences in field ordering, whitespace, numeric precision, or the handling of optional fields like PhantomData.
Conclusion
Message [msg 1637] is a masterclass in systematic debugging. The assistant takes a seemingly narrow observation — that CuZK ignores the gRPC registered_proof field — and uses it to eliminate an entire class of potential bugs. Through careful reasoning about the JSON round-trip, cross-referencing with the working non-CuZK path, and precise identification of the remaining structural difference (the C1OutputWrapper), the assistant transforms a vague "something is wrong with PSProve + CuZK" into a focused hypothesis about wrapper-layer serialization. This message marks the transition from exploration to targeted investigation, setting the stage for the deep dive into Rust serde_json deserialization that would follow.