Quickstart
Last updated
Last updated
This guide will help you set up a basic example that folds a circuit that describes the identify function . The entire code can be found at https://github.com/snarkify/sirius-quickstart.
Install rust with rustup
Create a new project with cargo
Add 'sirius' to dependencies
Full code can be found at https://github.com/snarkify/sirius-quickstart/blob/main/src/main.rs
Let's break down each section of this code
First, import the necessary modules and types from the Sirius library:
This block imports modules for handling fields, circuits, and the incremental verification process. It also sets up the types for the BN256 and Grumpkin elliptic curves.
Define constants and the structure for your custom circuit:
Explanation:
Folding Steps (FOLD_STEP_COUNT
): Specifies the number of steps the IVC process will execute. Each step represents a round of computation in the circuit.
Arity (A1
and A2
): Defines the input/output size per step for the primary and secondary circuits. Set to 1
in this example, meaning one input and one output per step.
Initial Inputs (PRIMARY_Z_0
and SECONDARY_Z_0
): These are the starting values passed to the circuits at the zero step. Here, they are initialized to zero (C1Scalar::ZERO
and C2Scalar::ZERO
), but in a real circuit, you should replace these with the actual values required by your circuit.
Commitment Key Size (PRIMARY_COMMITMENT_KEY_SIZE
and SECONDARY_COMMITMENT_KEY_SIZE
): Defines the size of the commitment keys for the circuits. Start with the provided values, but increase if your circuit requires more.
Table Size (PRIMARY_CIRCUIT_TABLE_SIZE
and SECONDARY_CIRCUIT_TABLE_SIZE
): Specifies the lookup table sizes. A minimum of 17 is needed, but increase the size if your circuit demands more complex operations.
Next, define a basic implementation of the StepCircuit trait:
In this section, the StepCircuit
trait is implemented for the MyStepCircuit
struct:
struct MyConfig
Acts as a template for configuring your circuit. Although it's empty in this example, it usually stores important constrain system configuration details.
struct MyStepCircuit
Represents the actual step-circuit logic. In this example, it simply returns the input unchanged, making it a trivial circuit. This is useful as a starting point or for testing purposes, but real circuits would involve more complex operations.
fn configure
This method is where the circuit's configuration is set up. It initializes any necessary columns & gates & lookups in the constraint system. The method currently returns an empty configuration, but in a more advanced circuit, it would be populated with the relevant data.
fn synthesize_step
This is the core of the circuit, where the computation for each step happens. It takes the configuration and an array of input variables (z_i), and returns the next step's output. In this trivial example, the output is simply the input, but this is where you would implement the actual logic for your step-circuit's step function.
This example serves as a basic template. When developing your own circuits, you would replace the trivial operations with the actual logic needed for your application.
Check `StepCircuit` trait for more details
Next, we need to set up the commitment keys for our circuits. This involves generating or loading keys for the elliptic curves used in the primary and secondary circuits.
Explanation:
Storing Generated Keys: The commitment keys are generated and stored in the .cache
folder. This is done to avoid the need to regenerate the keys every time you run the program, which saves time and computational resources.
First Key Generation: The first time the keys are generated, it can take a significant amount of time. This is because key generation for cryptographic curves involves complex computations. Once generated, these keys are cached for future use.
Elliptic Curves: In this example, keys are generated for two different elliptic curves: bn256
for the primary circuit and grumpkin
for the secondary circuit.
Use of unsafe
: The unsafe
block is necessary because the functions that load or generate the keys operate directly on memory-mapped files. This approach is used for performance reasons - mapping keys directly from memory is faster than serializing and deserializing them. However, it also requires careful handling to ensure the integrity of the cached files.
This setup is crucial for ensuring that your circuits can efficiently use these cryptographic keys without incurring unnecessary delays in key generation each time the program is executed.
Explanation:
new_default_pp
: This function initializes the PublicParams
with mostly default settings. It simplifies the setup process by automatically configuring the majority of parameters. However, if your circuit requires specific customizations or more advanced settings, you should create the PublicParams
manually, specifying types and values directly.
PublicParams
: This structure not only store essential configuration data but also lock in the structure of the PLONKish constraint system for the entire IVC process.
When you create pp
, the configure
and synthesize
methods of your circuits are called internally to collect and organize the constraints, ensuring consistency across all folding steps in the IVC.
Check `PublicParams` Struct for more details
Finally, perform the folding steps and verify the computation:
Explanation:
IVC::new
: This function initializes the Incrementally Verifiable Computation (IVC) instance and automatically performs the zero step using the provided initial inputs (PRIMARY_Z_0
and SECONDARY_Z_0
). This zero step sets up the initial state for the folding process, ensuring that the IVC begins with the correct starting conditions.
IVC::fold_step
: During each call to fold_step, the circuit's configure and synthesize methods are invoked few times. These methods define and execute the circuit's constraints and logic for that step. In a real-world circuit, you should update the circuit's witness (private inputs) between steps to reflect the changing state of the computation. This ensures that each folding step processes the correct data as the IVC progresses.
'DebugMode': The debug_mode argument (set to true in this example) enables additional checks at each folding step. Specifically, it invokes the MockProver
from the Halo2 library to verify the correctness of the computation at each step. While this mode is invaluable for debugging and ensuring correctness during development, it introduces a performance overhead. If you disable debug_mode
for performance reasons (by setting it to false), errors will only be detected at the end of the entire computation during the call to IVC::verify
.
IVC::verify
: The verify method is called at the end of the IVC process to check the validity of the entire computation. However, it only reports whether the computation was successful or if an error occurred at some point during the process. If debug_mode
is disabled, this method lacks the detailed information to pinpoint where the error happened—only whether the final result is valid or not.
Check `IVC` struct for more details
To run the example for the first time, use the following command:
This will compile the project in release mode, which is optimized for speed. During this initial run, the Set Up Commitment Keys for the BN256 and Grumpkin curves will be generated and cached. This process may take some time, so running in release mode ensures it completes as quickly as possible.
For subsequent runs, you can use the following command without the --release flag:
This will reuse the previously generated commitment keys, so the process will be faster, and there’s no need to recompile in release mode unless you're making significant changes or need the performance optimization again.
When the example runs successfully, you should see output indicating that the folding steps were executed and verified successfully:
In this Quickstart, we explored setting up a basic example with Sirius IVC, including initializing with PublicParams
, performing folding steps, and understanding the role of debug mode and verification. We implemented a trivial step circuit that returns its input unchanged, laying the groundwork for more complex circuits.
For a deeper dive into implementing a real step circuit with dynamic witness management, check out the Fold a Summation Circuit section.