Modular Prover Design
This page provides an overview of the Zircuit Prover design, capturing its components and the design principles that were applied.
Last updated
This page provides an overview of the Zircuit Prover design, capturing its components and the design principles that were applied.
Last updated
Zircuit built a prover and an architecture around it that abstracts the underlying zkEVM tech stack. This results in a modular stack that enables the proof system, circuits, commitment scheme, curve, and recursion scheme to be replaced easily.
All these modules are replaceable with minimal effort. Decision-making regarding circuit topology is centralized in a single module, namely the composition module.
Each module is focused solely on its specific function. In cases where invoking other modules is necessary, interfaces or traits are used instead of direct invocations to maintain independence.
Each module is self-contained and focused on what it does, ensuring clear boundaries and responsibilities within the system.
The system is designed to easily scale and accommodate additional modules or increased complexity without requiring significant redesign. This principle aims to future-proof the system against evolving requirements and technological advancements.
To fulfil our design principles and ensure consistency across development, we defined a set of traits that our system components must adhere to.
This trait defines the parameters that the underlying proof system needs to compute proofs. It abstracts the scheme and exposes only high-level APIs, encapsulating the lower logic from outside components. The exposed APIs are setup, load, and verify. Setup is almost never used except in development and testing. Load is the method that prepares the parameters to be used by the proof system, and verify is a method to carry out a security check on the parameters to see if they are intact.
The prover is comprised of three associations, including two type associations and a params association. The prover obviously needs parameters to compute proofs. The associated types are witness and operations. The witness is a type that the prover must implement, defining how the input blocks are witnessed. The operations define which actions the prover supports. These actions are atomic proving operations that can be orchestrated to achieve high-level business logic. An L2 block proving requires all zkEVM circuits to be proved in parallel. The breakdown of the logic is captured in the operations. Each operation is implemented by the prover in the execute method.
To an outside application, how the prover works is a black box. All they need to know is which operations are supported by the prover and which witness type they should pass in. This is an abstraction layer to ensure changes in the prover never break the proving pipeline (the applications that use the prover). The consumer binary, while unaware of the prover logic, can run supported operations/steps on each prover and output the result.
To fulfil a business logic such as L2 block proving, multiple steps need to be orchestrated in parallel and sequentially. Without getting into details of how proving is done or getting too specific to a particular implementation, one can imagine that proving an L2 block involves various circuit proving, recursions, data availability commitment publishing, batching, and so forth. The decision on how to carry out the operations is often hardcoded in the pipeline. This is problematic because any changes (due to a new emerging technique) would require changes in the proving pipeline and can be costly and error-prone due to the brittle nature of these operations. The composer abstracts away the orchestration logic in terms of what has to be done in which order. It has one method, run_operation
, which takes an input (the output of the last executed operation) and outputs one result. It essentially calls the consumer with the correct step to be executed next based on the current step results it receives as input. The composer can support multiple consumers enabling prover versioning, for instance, using the block number.
This prover implements the prover trait and is used by Zircuit, encapsulating several disjoint or interchangeable dependencies: zkEVM circuits, Snark Verifier, Params, and halo2 Proof System.
The Zircuit consumer exposes all the steps that are supported by the Zircuit prover.
The Zircuit composer defines the orchestration steps that need to be carried out to complete the proving of our blocks.
The beneficial aspect of this design is that each component can be updated, upgraded, or replaced without the need to touch any other part of the system. For example, if new zkEVM circuits developed by the community are orders of magnitude faster to prove, our modular prover can be updated with very modest efforts, requiring only the replacement of the zkEVM dependency and the gluing code within the prover itself. Another example is supporting a GPU-based proof system (such as halo2) by patching the dependency and nowhere else in the code.