The Data Model Bridge: How a Single Edit to types.rs Enabled Multi-Proof Support in cuzk

Message Overview

The subject message (msg id=294) is deceptively brief:

[assistant] [edit] /home/theuser/curio/extern/cuzk/cuzk-core/src/types.rs Edit applied successfully.

Two lines, one file edit, zero visible diff. Yet this message represents a critical architectural pivot in the cuzk proving engine's Phase 1 implementation. It is the moment when the data model evolved from a single-purpose PoRep-only struct into a polymorphic carrier capable of expressing all four Filecoin proof types: PoRep C2, WinningPoSt, WindowPoSt, and SnapDeals. Understanding why this edit was necessary, what knowledge it depended on, and what it unlocked reveals the deep interplay between protocol design, serialization formats, and distributed systems architecture that characterizes the entire cuzk project.

The Context: From PoRep-Only to Four Proof Types

To appreciate the significance of this edit, one must understand what came before it. The cuzk proving engine, as described in the project plan document cuzk-project.md ([msg 284]), was conceived as a persistent GPU-resident SNARK proving daemon — analogous to how vLLM serves inference models. Phase 0 had successfully scaffolded the daemon with a single proof type: PoRep C2 (the computationally heaviest Filecoin proof, consuming ~200 GiB of peak memory and taking ~360 seconds per proof). The Phase 0 ProofRequest struct in types.rs reflected this narrow focus: it carried a single vanilla_proof: Vec<u8> field, a sector_number, a miner_id, and a randomness byte array. This was sufficient for PoRep C2 because that proof type consumes exactly one vanilla proof (the C1 output) per request.

Phase 1, as defined in the project plan and the total impact assessment document (c2-total-impact-assessment.md, also read in [msg 284]), demanded support for three additional proof types: WinningPoSt (the time-critical proof that must complete within a Filecoin epoch), WindowPoSt (a per-partition proof with multiple sectors), and SnapDeals (a sector update proof with three commitment CIDs). Each of these proof types has fundamentally different input requirements.

The assistant's research in the preceding messages ([msg 286] through [msg 293]) had uncovered these differences through exhaustive codebase exploration. By reading every source file in the cuzk workspace, examining the filecoin-proofs-api function signatures, and studying Curio's Go FFI layer, the assistant discovered critical details that would shape the data model changes. For instance, PoSt proofs require multiple vanilla proofs per request — one per sector being proven. WindowPoSt in particular can involve proving dozens of sectors in a single partition, each with its own vanilla proof. SnapDeals, meanwhile, needs three separate commitment CIDs (comm_r_old, comm_r_new, comm_d_new) in addition to its vanilla proofs.

What the Edit Actually Changed

While the message itself does not show a diff, the surrounding context reveals exactly what was modified. The assistant had just read the full contents of types.rs in [msg 293] and had a detailed todo list from [msg 291] specifying:

"Phase 1.2: Update types.rs — add vanilla_proofs: Vec<Vec<u8>> field to ProofRequest"

The protobuf definition had already been updated in [msg 292] to change bytes vanilla_proof = 10 (a single byte field) to repeated bytes vanilla_proofs = 10 (a repeated field, i.e., a list of byte arrays). The types.rs edit was the Rust-side mirror of this change, transforming the ProofRequest struct's vanilla_proof: Vec<u8> field into vanilla_proofs: Vec<Vec<u8>>.

Additionally, the edit renamed the SnapDeals-related fields. The original protobuf and types had fields named sector_key_cid, new_sealed_cid, and new_unsealed_cid. The assistant's research into the filecoin-proofs-api function signatures (documented in [msg 287]) revealed that the actual API function generate_empty_sector_update_proof_with_vanilla expects parameters named comm_r_old, comm_r_new, and comm_d_new — the standard Filecoin terminology for commitment replicas. Renaming the struct fields to match the API nomenclature eliminated a source of confusion and made the code self-documenting.

The Reasoning: Why This Edit Was Necessary

The edit was not merely cosmetic. It was the structural prerequisite for everything that followed. Without this change, the prover module (prover.rs) could not have been rewritten to call the three new proof functions. The ProofRequest struct is the universal input type that flows through the entire engine pipeline: it is created by the gRPC service layer from protobuf messages, passed to the scheduler for priority queuing, dispatched to a GPU worker, and finally consumed by the prover. Every layer reads from this struct. Changing it rippled through the entire codebase.

The assistant's decision to use Vec<Vec<u8>> (a list of byte vectors) rather than a more structured type was a deliberate trade-off. The alternative would have been to create an enum of proof-type-specific payloads, e.g.:

enum ProofPayload {
    PoRep { vanilla_proof: Vec<u8> },
    WinningPoSt { vanilla_proofs: Vec<Vec<u8>>, randomness: [u8; 32] },
    WindowPoSt { vanilla_proofs: Vec<Vec<u8>>, partition_index: u32 },
    SnapDeals { vanilla_proofs: Vec<Vec<u8>>, comm_r_old: [u8; 32], comm_r_new: [u8; 32], comm_d_new: [u8; 32] },
}

While more type-safe, this approach would have introduced significant complexity: the scheduler would need to pattern-match on the enum to extract fields, the gRPC service would need to construct different enum variants, and every new proof type would require adding a new variant. The flat Vec&lt;Vec&lt;u8&gt;&gt; approach, combined with the existing proof_kind enum field and the optional commitment fields, kept the data model simple and extensible. This reflects a pragmatic engineering philosophy: prefer flat data structures over deep type hierarchies when the primary consumer (the prover) will switch on the proof_kind enum anyway.

Assumptions Made

The edit embodied several assumptions, most of which were well-justified by the preceding research:

Assumption 1: The protobuf repeated bytes field maps cleanly to Vec&lt;Vec&lt;u8&gt;&gt; in Rust. This is correct for the tonic/prost codegen used by the cuzk workspace. The repeated bytes protobuf type generates a Vec&lt;Vec&lt;u8&gt;&gt; field in the generated Rust struct, which can be directly assigned to the ProofRequest field.

Assumption 2: The existing PoRep C2 path can continue to use vanilla_proofs as a single-element vector. The assistant could have kept a separate vanilla_proof field for backward compatibility, but instead chose to unify on the plural form. This meant the PoRep C2 code path would need to wrap its single vanilla proof in a vec![vanilla_proof] — a trivial change that preserved uniformity across all proof types.

Assumption 3: The SnapDeals commitment fields should be renamed to match the upstream API. This was a correctness assumption: the filecoin-proofs-api function generate_empty_sector_update_proof_with_vanilla takes comm_r_old, comm_r_new, and comm_d_new as separate byte array parameters. By naming the struct fields identically, the mapping from the protobuf message to the Rust function call became mechanically straightforward — a simple field-by-field assignment with no semantic translation.

Assumption 4: The registered_proof numeric field (already present in the struct) is sufficient to select the correct FFI function. The assistant had verified in [msg 287] that the RegisteredSealProof, RegisteredPoStProof, and RegisteredUpdateProof enums all derive from the same underlying integer representation, and that the Go FFI layer passes these values through as #[repr(i32)] C enums. This meant the existing registered_proof: u64 field could serve all proof types without modification.

Input Knowledge Required

Understanding this edit requires knowledge spanning several domains:

  1. Filecoin proof types and their input structures: The reader must know that WinningPoSt requires a randomness seed and a list of sector vanilla proofs; that WindowPoSt is partitioned with one proof per partition; and that SnapDeals involves three commitment CIDs representing the old and new sealed replicas and the new unsealed data.
  2. Protobuf-to-Rust mapping: The repeated bytes protobuf type generates Vec&lt;Vec&lt;u8&gt;&gt; in the prost-generated code. The types.rs struct must match this generated type for the gRPC service layer to function.
  3. The filecoin-proofs-api function signatures: The assistant had spent significant effort in [msg 287] and [msg 296][msg 307] tracing the exact parameter types and return types of the three new proving functions. The edit was informed by this research.
  4. The existing types.rs structure: The edit built on top of the existing ProofRequest struct, which already had fields for proof_kind, sector_size, registered_proof, priority, sector_number, miner_id, and randomness. The assistant had read this file in full in [msg 293].
  5. The Curio Go FFI serialization format: Research in [msg 288] had revealed how Curio's Go code serializes vanilla proofs for each proof type, confirming that PoSt proofs are indeed lists of per-sector vanilla proofs.

Output Knowledge Created

The edited types.rs produced several forms of knowledge:

  1. A universal proof request format: Any code that needs to submit a proof to the cuzk engine now has a single, well-defined struct to populate. This simplifies the gRPC service layer, the scheduler, and the prover.
  2. A clear mapping from protobuf to internal types: The struct fields now mirror the protobuf fields exactly, making the conversion in service.rs a mechanical translation with no semantic ambiguity.
  3. Documentation of the proof-type-specific field semantics: The renamed SnapDeals fields (comm_r_old, comm_r_new, comm_d_new) document their purpose directly in the type system, whereas the previous names (sector_key_cid, new_sealed_cid, new_unsealed_cid) were ambiguous.
  4. A foundation for the prover implementations: The subsequent edits to prover.rs ([msg 307] and beyond) could now reference request.vanilla_proofs[i] for each vanilla proof in a PoSt request, and request.comm_r_old for the SnapDeals old commitment, without any additional parsing or deserialization.

The Thinking Process

The assistant's thinking process, visible across messages [msg 285] through [msg 294], reveals a systematic, research-first approach to software engineering. Rather than diving directly into code changes, the assistant:

  1. Reviewed the project plan ([msg 284]) to understand Phase 1's scope and deliverables.
  2. Read every source file in the workspace ([msg 286]) to understand the current state.
  3. Researched the upstream API ([msg 287]) to find exact function signatures.
  4. Studied the Curio FFI layer ([msg 288]) to understand serialization formats.
  5. Examined the protobuf definition ([msg 289]) to plan schema changes.
  6. Checked the service layer ([msg 290]) to understand how protobuf types map to core types.
  7. Created a detailed plan ([msg 291]) with prioritized subtasks.
  8. Applied changes in dependency order: protobuf first ([msg 292]), then types.rs (this message), then prover.rs. This ordering is crucial: the protobuf change defined the schema, the types.rs change defined the internal representation, and the prover.rs change would consume it. Each layer depends on the layer below. By applying changes from the outermost layer (protobuf, the contract with clients) inward to the innermost layer (prover, the implementation), the assistant ensured that each step was built on a solid foundation.

Conclusion

The edit to types.rs in message 294 is a textbook example of how a small, well-informed change to a data model can unlock significant architectural capability. In two lines of output, the assistant transformed the cuzk engine from a PoRep-only prototype into a multi-proof proving system capable of handling all Filecoin proof types. The edit itself was trivial — a field type change and a few renames — but the knowledge that informed it was the product of hours of systematic research across multiple codebases, API layers, and serialization formats. This is the essence of effective systems engineering: the most impactful changes are often the simplest, provided they are grounded in deep understanding of the system's constraints and requirements.