Technical
Sep 4, 2024
This review aims to scope out the issues in Valida which must be addressed for an MVP. This review is complementary to but not redundant with the October 2023 Plonky3 / Valida review. That review addressed an earlier version of the code which had some issues that are not present in the current version. Also, this review tries to go deeper into certain issues that were not fully addressed in the October 2023 review, while also not going as deep into certain issues that were addressed in the October 2023 review.
This review should not be considered a comprehensive internal audit of Valida. The goal is not to ensure that Valida is correct; this would require a deeper level of review than is undertaken here. Instead, the goal is to flush out critical issues required by the MVP which can be readily flushed out at this point via code review. The main focus of this review is checking the STARK implementation for soundness and completeness. Proving the STARK implementation sound and complete is not the goal, nor is that achieved in this review. This review does point out some issues with the current STARK implementation.
Soundness and completeness are the two basic correctness properties which all proving systems must satisfy. Soundness means basically that one cannot create a proof of a false statement (or the likelihood of doing so is negligible). Completeness means basically that the prover can create a proof of any statement for which one has a witness to the statement’s truth (or the probability that the prover cannot do this is negligible). Strictly speaking, soundness is a property of a verifier (it will not accept a proof of a false statement), while completeness is a property of a prover / verifier pair (for every true statement of the type covered, the prover can prove it given a witness, and the verifier accepts that proof).
Proofs of the soundness and completeness of the Valida prover / verifier pair would use as premises the soundness and completeness of the FRI polynomial commitment scheme (PCS) which the Valida prover / verifier pair is built on. Proofs of the soundness and completeness of the FRI PCS would in turn use as premises the soundness and completeness of the FRI low degree test (LDT) which the FRI PCS is built on. This review does not cover the FRI PCS or the FRI LDT. This is not because they are not worthy of further review, but because they are not in the scope of the present review, which focuses on the Valida code only and not the Plonky3 code.
Two other correctness properties we care about, in addition to soundness and completeness, are succinctness and zero knowledge. Succinctness means basically that the proofs are small enough and easy enough to verify. Zero knowledge means basically that having the proofs does not help to find the inputs used to generate the proofs, any more than knowing the facts that they prove helps to find those inputs.
Currently, Valida proofs are not zero knowledge. To make them zero knowledge, we have to introduce blinding factors at appropriate points.
Another correctness property we care about is functional completeness, which means roughly that the VM must be able to run the programs output by the compiler backend.
This review does not try to be the last word on the correctness properties of Valida. The soundness and completeness of Valida have never been proven, as far as I know. I don’t claim that fixing the open issues outlined in this review would result in Valida’s prover / verifier pair being sound and complete, or that it would result in Valida being functionally complete. There are no proofs cited or included in this review. Much more verification work remains to be done on Valida. This review is only a partial contribution towards the goal of verifying Valida and scoping out the remaining work to bring Valida up to spec.
The STARK argument used in Valida follows a common style of argumentation used in zero knowledge proofs. It commits to traces represented as polynomials. It commits to the quotient of a linear combination of all of the constraint polynomials over the zerofier polynomial of the evaluation domain used to generate the polynomials from the traces. The statement that this quotient exists implies with high probability that all of the constraint polynomials are zero at all of the rows, i.e., at all of the points in the evaluation domain where the traces live along the graphs of their polynomials.
The verifier succinctly verifies that the quotient exists by dealing with polynomial commitments sent by the verifier, along with openings of those polynomials at pseudo-random points selected using the Fiat-Shamir heuristic. These commitments and openings allow the verifier to succinctly verify that at a pseudo-randomly selected point, the equation P(x) = Q(x) * Z(x) holds, for some Q(x), where P(x) is some randomized linear combination of the constraint polynomials over the trace polynomials committed to in the proof.
Valida uses a collection of STARK arguments to prove a single program execution. Each STARK argument is of the form just described, succinctly proving / verifying equations of the form P(x) = Q(x) * Z(x), for P(x) a randomized linear combination of constraint polynomials. Each chip should have its own STARK argument (at least one, but possibly more than one).
Some chips need to interact with each other. Each chip has its own trace, and sometimes these traces need to be related to each other in certain ways for coherence to be achieved. For example, the memory chip trace needs to be consistent with itself (with every read resulting in the last value written to the same address), but the memory chip trace also needs to be consistent with the CPU chip trace (so that every LOAD32 and STORE32 instruction trace is consistent with the memory trace). The former (memory consistency) is a constraint on a single chip’s trace (the memory trace), and the latter (memory consistency with the CPU chip trace) is a constraint relating two chip traces (the memory trace and the CPU chip trace).
Cross-chip interactions are defined using an Interaction abstraction defined in the machine crate of the Valida code. Interactions are of four types: local send, local receive, global send, and global receive. Each interaction defines the columns it covers, the count column used to keep track of the interaction, and the index of the bus argument used to prove the interaction.
Each bus argument is either a local or a global bus argument. The difference between these is that a local bus argument is local to a single chip, whereas a global bus argument is non-local across multiple chips. It’s not clear to me what difference this makes in practice, since they are treated similarly in the prover / verifier pair. A local bus argument is usable as a form of lookup argument. A local bus argument occurs only in the memory chip.
All of the interactions between all of the chips on a machine are proven together in a single permutation argument. This permutation argument claims that a cumulative sum of terms is equal to zero. Each interaction of the permutation argument trace contributes to the cumulative sum the multiplicative inverse of the value of a randomized linear combination of the elements of the fields of that interaction. These randomized linear combinations are meant to be such that the terms in the cumulative sum for corresponding sends and receives cancel each other out. Hypothetically, this is unlikely to occur unless the sends and receives do in fact correspond one to one and are consistent with each other. This hypothesis would be a key lemma in a proof of soundness of the permutation argument.
The accompanying spreadsheet summarizes the constraints for the chips in the basic machine and the interactions between them.
The following versions of Valida and Plonky3 were referenced in this review:
To address whether the Valida prover / verifier pair is sound and complete or not, the following issues need to be considered:
This review considered each of these issues. No problems were identified with the STARK argument itself or with the permutation argument; at the same time, the presence of bugs in these pieces was not ruled out.
There are no known cases where a valid trace cannot be proven valid. More extensive testing should be performed to help flush out any cases where a valid trace is not provable.
There are some known issues and possible issues with the polynomial constraints and interactions, which mean that the polynomial constraints of the chips and the requirements of the interactions do not in every case suffice to ensure that the trace is valid. Those issues are as follows:
Overall, the verdict is that currently the Valida prover / verifier pair is not sound, and possibly it is complete (in the sense that all true statements in its language are provable), but a lot more could and should be done to verify that Valida’s prover / verifier pair is complete (in addition to making it sound).
Functional completeness means roughly that the VM must be able to run the programs which the compiler backend outputs. This means that functional completeness is a moving target, because the compiler is changing. The ISA (Instruction Set Architecture) is in theory the definition of the interface between the compiler and the VM. We are currently exploring options for the ISA and its definition is not pinned down.
Currently we think that the following instructions are going to be required in the ISA to support the compiler MVP:
All of the required instructions which are not supported (marked ❌ in the above table) need to be added to the VM and STARK constraints need to be implemented for them, for an MVP.
We think that the following instructions would be good to have in the ISA, but not strictly required:
Thanks to Daniel Lubarov and other teammates for answering some questions about the memory argument, preprocessed traces, and the equality test in the CPU chip.