A STARK prover and verifier for arbitrary computations
A STARK prover and verifier for arbitrary computations
A STARK prover and verifier for arbitrary computations.
WARNING: This is a research project. It has not been audited and may contain bugs and security flaws. This implementation is NOT ready for production use.
A STARK is a novel proof-of-computation scheme to create efficiently verifiable proofs of the correct execution of a computation. The scheme was developed by Eli Ben-Sasson, Michael Riabzev et al. at Technion - Israel Institute of Technology. STARKs do not require an initial trusted setup, and rely on very few cryptographic assumptions. See references for more info.
The aim of this project is to build a feature-rich, easy to use, and highly performant STARK prover which can generate integrity proofs for very large computations.
Winterfell is a fully-functional, multi-threaded STARK prover and verifier with the following nice properties:
A simple interface. The library provides a relatively simple interface for describing general computations. See usage for a quick tutorial, air crate for the description of the interface, and examples crate for a few real-world examples.
Randomized AIR support. The library supports multi-stage trace commitments, which enables support for randomized AIR. This greatly increases the expressivity of AIR constraints, and enables, among other things, multiset and permutation checks similar to the ones available in PLONKish systems.
Multi-threaded proof generation. When compiled with concurrent feature enabled, the proof generation process will run in multiple threads. The library also supports concurrent construction of execution trace tables. The performance section showcases the benefits of multi-threading.
Configurable fields and hash functions. The library is generic over the selection of fields (both base field and extension field) and hash functions (including arithmetization-friendly hashes). This simplifies fine-tuning of proof generation for specific performance and security targets. Some options for both are provided in the math and crypto crates, but the library can work with any implementation that complies with the specified interfaces.
WebAssembly support. The library is written in pure Rust and can be compiled to WebAssembly. The std standard library is enabled as feature by default for both prover and verifier crates. For WASM targets, one can compile with default features disabled by using --no-default-features flag.
Async prover. The library supports both sync and async variants of the Prover trait. By default, the sync version is exported. The async version of the trait can be enabled via the async feature flag.
Over time, we hope extend the library with additional features:
Perfect zero-knowledge. The current implementation provides succinct proofs but NOT perfect zero-knowledge. This means that, in its current form, the library may not be suitable for use cases where proofs must not leak any info about secret inputs.
The project is organized into several crates like so:
| Crate | Description |
|---|---|
| examples | Contains examples of generating/verifying proofs for several toy and real-world computations. |
| prover | Contains an implementation of a STARK prover which can be used to generate computational integrity proofs. |
| verifier | Contains an implementation of a STARK verifier which can verify proofs generated by the Winterfell prover. |
| winterfell | Re-exports prover and verifier crates as a single create for simplified dependency management. |
| air | Contains components needed to describe arbitrary computations in a STARK-specific format. |
| fri | Contains implementation of a FRI prover and verifier. These are used internally by the STARK prover and verifier. |
| math | Contains modules with math operations needed in STARK proof generation/verification. These include: finite field arithmetic, polynomial arithmetic, and FFTs. |
| crypto | Contains modules with cryptographic operations needed in STARK proof generation/verification. Specifically: hash functions and Merkle trees. |
| utils | Contains a set of utility traits, functions, and macros used throughout the library. |
Generating STARK proofs for a computation is a relatively complicated process. This library aims to abstract away most of the complexity, however, the users are still expected to provide descriptions of their computations in a STARK-specific format. This format is called algebraic intermediate representation, or AIR, for short.
This library contains several higher-level constructs which make defining AIRs for computations a little easier, and there are also examples of AIRs for several computations available in the examples crate. However, the best way to understand the STARK proof generation process is to go through a trivial example from start to finish.
First, we'll need to pick a computation for which we'll be generating and verifying STARK proofs. To keep things simple, we'll use the following:
use winterfell::math::{fields::f128::BaseElement, FieldElement};
fn do_work(start: BaseElement, n: usize) -> BaseElement {
let mut result = start;
for _ in 1..n {
result = result.exp(3) + BaseElement::new(42);
}
result
}This computation starts with an element in a finite field and then, for the specified number of steps, cubes the element and adds value 42 to it.
Suppose, we run this computation for a million steps and get some result. Using STARKs we can prove that we did the work correctly without requiring any verifying party to re-execute the computation. Here is how to do it.
First, we need to define an execution trace for our computation. This trace should capture the state of the computation at every step of its execution. In our case, the trace is just a single column of intermediate values after each execution of the loop. For example, if we start with value 3 and run the computation for 1,048,576 (same as 220) steps, the execution trace will look like this:
| Step | State |
|---|---|
| 0 | 3 |
| 1 | 69 |
| 2 | 328551 |
| 3 | 35465687262668193 |
| 4 | 237280320818395402166933071684267763523 |
| ... | |
| 1,048,575 | 247770943907079986105389697876176586605 |
To record the trace, we'll use the TraceTable struct provided by the library. The function below, is just a modified version of the do_work() function which records every intermediate state of the computation in the TraceTable struct:
…Next, we need to define algebraic intermediate representation (AIR) for our computation. This process is usually called arithmetization. We do this by implementing the Air trait. At the high level, the code below does three things:
For more information about arithmetization see air crate, and here is the actual code:
…Next, we need define our prover. This can be done by implementing [Prover] trait. The trait is pretty simple and has just a few required methods. Here is how our implementation could look like:
…Now, we are finally ready to generate a STARK proof. The function below, will execute our computation, and will return the result together with the proof that the computation was executed correctly.
…We can then give this proof (together with the public inputs) to anyone, and they can verify that we did in fact execute the computation and got the claimed result. They can do this like so:
…That's all there is to it! As mentioned above, the examples crate contains examples of much more interesting computations (together with instructions on how to compile and run these examples). So, do check it out.
The Winterfell prover's performance depends on a large number of factors including the nature of the computation itself, efficiency of encoding the computation in AIR, proof generation parameters, hardware setup etc. Thus, the benchmarks below should be viewed as directional: they illustrate the general trends, but concrete numbers will be different for different computations, choices of parameters etc.
The computation we benchmark here is a chain of Rescue hash invocations (see examples for more info). The benchmarks were run on Intel Core i9-9980KH @ 2.4 GHz and 32 GB of RAM using all 8 cores.
Chain length
Trace time
96 bit security
128 bit security
R1CS equiv.
Proving time
Proof size
Proving time
Proof size
210
0.1 sec
0.04 sec
51 KB
0.07 sec
102 KB
218 constr.
212
0.4 sec
0.14 sec
65 KB
0.25 sec
128 KB
220 constr.
214
1.4 sec
0.6 sec
80 KB
1 sec
156 KB
222 constr.
216
6 sec
2.5 sec
94 KB
4 sec
184 KB
224 constr.
218
24 sec
11 sec
112 KB
18 sec
216 KB
226 constr.
220
94 sec
50 sec
128 KB
89 sec
252 KB
228 constr.A few remarks about these benchmarks:
Let's benchmark another example. This time our computation will consist of verifying many Lamport+ signatures (see example). This is a much more complicated computation. For comparison, execution trace for Rescue hash chain requires only 4 columns, but for Lamport+ signatu
No open issues yet, or sync has not completed.