The Architecture of Remote Proving: Splitting Vanilla Proofs from SNARK Computation

Introduction

In the long-running development of the cuzk remote GPU proving daemon for the Filecoin Curio storage proving system, there arrives a moment of architectural clarity—a message where the assistant crystallizes the core design challenge and commits to a specific integration strategy. Message [msg 3413] is that moment. In it, the assistant examines the existing PoRepSnark function, identifies the natural seam between two distinct computational phases, and decides how to split them across the local-remote boundary. This message is not about writing code; it is about deciding how to write code. It is a planning message, a reasoning message, and a design message all in one.

Context: The cuzk Integration

To understand message [msg 3413], one must understand the broader arc of the session. The assistant has been working for many rounds on a project called "cuzk"—a remote GPU proving daemon designed to offload the most memory-intensive and GPU-bound portions of Filecoin proof generation from the Curio storage proving node. The problem is stark: Filecoin's Groth16 SNARK proofs require enormous GPU memory (peaking at ~200 GiB), and running these proofs on every Curio node is expensive and inefficient. The cuzk daemon centralizes GPU proving onto dedicated hardware, allowing Curio nodes to focus on the CPU-bound work they do best: generating "vanilla proofs" that require access to local sector data.

By message [msg 3413], the assistant has already:

The Message: Reasoning and Design Decisions

The message opens with a declarative statement that immediately frames the problem:

The key insight: PoRepSnark does two things: 1. GeneratePoRepVanillaProof — generates vanilla proof (CPU, on the machine that has the sector data) 2. SealCommitPhase2 — runs the SNARK computation (GPU)

This is the foundational observation. The assistant has read the existing PoRepSnark function in lib/ffi/sdr_funcs.go and recognized that it is not a monolithic operation but a composition of two fundamentally different computations. The vanilla proof generation (GeneratePoRepVanillaProof) is a CPU-bound operation that requires access to the sector's sealed and unsealed data—it must run on the machine that physically stores the sector. The SNARK computation (SealCommitPhase2) is a GPU-bound operation that takes the vanilla proof as input and produces the final Groth16 proof—it can run anywhere.

This insight is the key that unlocks the entire integration architecture. By recognizing this natural split, the assistant can design a system where each phase runs on the most appropriate hardware: vanilla proofs stay local where the data lives, and SNARK proofs are shipped to the cuzk daemon where the GPUs live.

The assistant then verifies the return type of GeneratePoRepVanillaProof:

Let me check what GeneratePoRepVanillaProof returns — it's []byte (the JSON-serialized C1 output).

This is a critical detail. The vanilla proof is serializable—it is just a blob of bytes (specifically, JSON-serialized C1 output from the Filecoin proof pipeline). This means it can be transmitted over gRPC to the cuzk daemon without any special handling. The assistant is confirming that the data boundary between the two phases is clean and network-friendly.

The Design Decision: New Methods vs. Modified Methods

The assistant then considers two approaches:

  1. Modify SealCalls.PoRepSnark to optionally delegate the SNARK phase to cuzk.
  2. Create new methods on SealCalls that explicitly split the phases. The assistant chooses the second approach:
Rather than modifying SealCalls.PoRepSnark, I'll create new methods on SealCalls that split the vanilla proof generation from the SNARK, then use the cuzk client in the task's Do method directly. Actually, simpler: add a PoRepSnarkCuzk method that generates vanilla locally, sends to cuzk, then verifies.

This is a deliberate architectural decision with several implications:

Preserving backward compatibility. The existing PoRepSnark method is used throughout the codebase—in PoRep tasks, in SnapDeals tasks, and potentially in other places. Modifying it to support remote proving would require adding conditional logic, configuration parameters, or callback mechanisms that would complicate the existing API. By creating a separate PoRepSnarkCuzk method, the assistant keeps the existing code path pristine while adding a new one.

Explicit intent. A method named PoRepSnarkCuzk makes the remote-proving intent explicit at the call site. When reading the task code later, a developer will immediately understand that this call goes through the cuzk daemon. This is far clearer than a modified PoRepSnark with a boolean flag or optional parameter.

Clean separation of concerns. The vanilla proof generation and SNARK computation are architecturally distinct responsibilities. Giving them distinct method signatures respects that distinction. The PoRepSnarkCuzk method has a clear contract: generate vanilla locally, ship to cuzk, return the result.

Testability. A separate method can be tested independently. The existing PoRepSnark continues to work as before, and the new method can be tested in isolation against a mock or real cuzk daemon.

The assistant also refines the approach mid-thought: initially considering splitting into two methods (one for vanilla, one for SNARK) and calling them from the task's Do method, then settling on the simpler PoRepSnarkCuzk wrapper that does both. This is a pragmatic choice—it reduces the number of integration points in the task code while still maintaining the architectural separation internally.

The Plan: A Dedicated Integration File

The message concludes with a concrete action:

Let me create a dedicated file for the cuzk integration methods on SealCalls:

And a bash command to inspect existing SealCalls methods to understand the pattern:

grep -n "^func (sb \*SealCalls)" ./lib/ffi/sdr_funcs.go | head -20

This reveals the assistant's working method: before writing new code, it examines existing patterns in the codebase. The SealCalls struct already has methods like GenerateSDR, TreeRC, and ensureOneCopy. By creating a new file for cuzk integration methods, the assistant keeps the integration code organized and separate from the existing FFI methods. This is a software engineering best practice: new functionality in new files, existing code left untouched.

Assumptions Embedded in the Message

Several assumptions underpin the reasoning in [msg 3413]:

The vanilla proof is self-contained and transmittable. The assistant assumes that the []byte output of GeneratePoRepVanillaProof contains all the information needed by the SNARK phase, and that it can be serialized and sent over gRPC without loss of fidelity. This is a reasonable assumption given the Filecoin proof specification, but it is an assumption nonetheless—if the SNARK phase required additional context (like sector metadata or configuration), the split would be more complex.

The cuzk daemon can accept the vanilla proof format. The assistant assumes that the cuzk daemon's API already supports receiving the JSON-serialized C1 output and running SealCommitPhase2 on it. This assumption is based on the earlier design work (Phases 10–12) where the daemon's proving pipeline was built, but the exact API contract for receiving vanilla proofs may not yet be finalized.

Network latency is acceptable. By splitting the proof into two phases with a network hop between them, the assistant implicitly assumes that the latency of sending the vanilla proof to cuzk and receiving the SNARK proof back is acceptable within Curio's task scheduling framework. For PoRep tasks that run on the order of minutes or hours, this is likely true, but it is an assumption that would need validation through benchmarking.

The SealCalls struct is the right integration point. The assistant chooses to add methods to SealCalls rather than, say, creating a standalone function or a new service layer. This assumes that SealCalls is the appropriate abstraction for proof-related operations, which is consistent with the existing codebase structure.

Input Knowledge Required

To fully understand message [msg 3413], one needs:

Knowledge of Filecoin proof structure. The distinction between "vanilla proof" (the CPU-generated intermediate output from the Filecoin proof pipeline) and "SNARK proof" (the final Groth16 proof generated by GPU computation) is central to the reasoning. Without this domain knowledge, the assistant's "key insight" would appear arbitrary.

Knowledge of the Curio codebase. Understanding that SealCalls is a struct in lib/ffi/sdr_funcs.go that wraps FFI calls, that PoRepSnark is the existing method for generating PoRep proofs, and that GeneratePoRepVanillaProof and SealCommitPhase2 are its internal steps—all of this context comes from the assistant's earlier research in the session.

Knowledge of the cuzk daemon architecture. The assistant has been building the cuzk daemon for many rounds. Understanding that it is a gRPC server that accepts vanilla proofs and returns SNARK proofs is necessary to see why the split makes sense.

Knowledge of Go and gRPC patterns. The assistant's reasoning about method signatures, file organization, and integration points assumes familiarity with Go programming conventions and gRPC client-server architecture.

Output Knowledge Created

Message [msg 3413] produces several pieces of knowledge that shape the subsequent implementation:

The architectural blueprint. The decision to split vanilla proof generation from SNARK computation is the foundational design choice for the entire cuzk integration. Every subsequent message in the session builds on this decision.

The method naming convention. PoRepSnarkCuzk establishes a naming pattern that will be extended to other proof types: SnapProveCuzk, ProveUpdateCuzk, and so on. This naming makes the remote-proving intent explicit throughout the codebase.

The file organization strategy. The plan to create a dedicated file for cuzk integration methods on SealCalls leads to the creation of lib/ffi/cuzk_funcs.go in subsequent messages. This keeps the integration code isolated and maintainable.

The integration point. By choosing to add methods to SealCalls rather than modifying the task code directly, the assistant establishes that the integration happens at the FFI layer, not at the task orchestration layer. This means the task code can remain relatively simple—it calls PoRepSnarkCuzk instead of PoRepSnark, and the complexity of the local-remote split is hidden inside the method.

The Thinking Process

What makes message [msg 3413] particularly interesting is the visible thinking process. The assistant does not simply state a conclusion; it walks through the reasoning step by step:

  1. Observe: PoRepSnark does two things.
  2. Analyze: One needs local data (vanilla proof), one needs GPU (SNARK).
  3. Verify: What does the vanilla proof look like? It's []byte—serializable.
  4. Consider alternatives: Modify existing method vs. create new method.
  5. Evaluate: New method is cleaner, preserves backward compatibility.
  6. Refine: Start with a split approach, then simplify to a wrapper method.
  7. Plan: Create a dedicated file, inspect existing patterns. This is classic software engineering reasoning: understand the existing system, identify the natural boundaries, design the integration to respect those boundaries, and implement with minimal disruption to existing code. The assistant is thinking like a systems architect, not just a code generator.

Conclusion

Message [msg 3413] is a pivotal moment in the cuzk integration. It is the message where the assistant stops researching and starts designing. The key insight—that PoRepSnark is really two operations that can and should be separated—unlocks the entire integration architecture. The decision to create new methods rather than modify existing ones preserves the integrity of the existing codebase while adding new capability. The plan to create a dedicated file keeps the integration organized and maintainable.

In the broader narrative of the session, this message represents the transition from analysis to synthesis. The assistant has spent many rounds understanding the existing system; now it begins to build the new one. The decisions made in this message—the split, the naming, the file organization, the integration point—will shape every subsequent message in the integration effort. It is a message about architecture, about clean boundaries, and about the art of adding new capabilities to a complex system without breaking what already works.