Replies: 11 comments 12 replies
-
Both the description and the examples remind me of vamp-ir. Perhaps the similarities are enough to join forces? |
Beta Was this translation helpful? Give feedback.
-
A few more thoughts on the potential structure of the constraint description file. Such a file could consist of several sections. A couple come to mind:
Declaration sectionIn the declaration section we need to define the set of columns we would be working with in both transition and boundary constraint sections. There are 3 sources of columns:
We could use a variation of destructuring syntax in many languages to name columns from various sources. To give a few examples:
The above says that the main execution trace should consist of 2 columns, and these columns could be referred to as In some cases, we may not need to name all columns. For example:
The above says that main trace should consist of 4 columns, but only the first two columns are named. In many situations it may be useful to group some columns together. We could use the following syntax for that:
The above says that the main trace should consist of 8 columns: individual columns Of course, we could declare a single variable to refer to the entire state like so:
We could name columns from the auxiliary trace in the same manner as columns of the main trace:
In the above, the main should consist of 4 columns while the auxiliary trace should consist of 2 columns named To add periodic columns, we could use
In the above, we define 2 periodic columns: the first one with a cycle length of 4 values, and the second one with the cycle length of 8 values. Another approach could be to break periodic column declaration into a separate section. For example:
It might also be useful to allow declaration of periodic columns in groups like so:
Periodic columns declared in the same group must have the same length. Constraints sectionIn the constraints section, we would define the actual constraints using column names defined in the declaration section. A few thoughts about potential notation:
A few examples:
We could use the above approach to work with auxiliary columns too. However, for auxiliary columns we also work with random values (provided by the verifier after the prover commits to the main trace), and the question arises as to how to refer to them. One option is to use some symbol - e.g., If we go with the symbol approach, constraint declaration could look as follows:
To enforce boundary constraints, we could use an
I think the above probably covers 60% - 70% of what we need. But a few open questions remain:
|
Beta Was this translation helpful? Give feedback.
-
Regarding modularity, I think what I sketched out in my original post could work. Specifically, we use the For example, let's say we have a module
Let's also say we have a module
Since the expected number of main and auxiliary columns for If the number of columns were the same, we could have done something like:
Or if
|
Beta Was this translation helpful? Give feedback.
-
One more thought about constants and variables: I think it would make sense to allow constants and variables of 3 different types:
Declaring constants could be done like so:
Referring to these constants inside expressions could be done with index notation:
Something like this could also be possible:
We could build variables from column references as well:
|
Beta Was this translation helpful? Give feedback.
-
It may be desirable to have support for simple functions. This could be beneficial for two reasons:
A good example for this could be MDS matrix multiplication for Rescue Prime constraints. This function needs to be called twice, and in the context of the prover, we could use a much more efficient routine to perform the multiplication (because we know we are in the base field). To make this feature as simple as possible, we could impose the following limitations:
A function declaration could look as follows:
This function takes a vector of 12 elements as a parameter, and returns a vector of 12 elements. To invoke this function, we could do something like this:
To support external subroutines, we could use something similar to Rust's attributes. For example:
The above would mean that when compiling the constraints in the context of the prover, instead of using the body of the function, an fn apply_mds(state: [Felt; 12]) -> [Felt; 12]; However, when the constraints are compiled in the context of the verifier, the body of the function would be used as is. SummarySummarizing the last several comments: the above probably covers 80% - 85% of what we need. Obviously, these are just sketches, and I'm sure things can be improved (and I probably have some inconsistencies here and there). However, the outstanding big questions in my mind now are:
|
Beta Was this translation helpful? Give feedback.
-
Regarding iteration: since we are always dealing with lists of known length, I'm inclined to restrict iteration to something similar to Python style list comprehension. For example, if we want to iterate over a list of columns and raise each element to some power, we could do it like this:
In both cases, the result would be a vector of 4 elements. We could also iterate over two lists simultaneously like so:
If the lists have different number of items, we could use range notation to make sure the number of items to iterate over is the same:
If we wanted to iterate over items and enumerate them, we could do it like so:
We could also support list folding. For example:
Or, an alternative syntax could be:
With this, we have enough tools to write constraints for something like Rescue Prime hash function. Such constraints would look like so:
|
Beta Was this translation helpful? Give feedback.
-
One alternative to the structure described previously is to introduce "evaluator" functions which would take the description of main and auxiliary traces as parameters. So, instead of doing something like this:
We could do something like this:
One advantage of this approach is that we can name evaluator functions and then invoke them from other places. For example:
Rescue Prime constraints in this format could look something like this:
And then we could invoke them from another constraint file like so:
|
Beta Was this translation helpful? Give feedback.
-
Here are some preliminary thoughts not addressing the bigger outstanding questions yet. It doesn't really include thoughts on the latest comment yet either
In a similar vein, I'd like things being as easy to read as possible, especially since we're thinking of this for purposes of auditing as well as for simplifying the writing process. Ideally, people should be able to read it without referencing a spec (especially if they're generally familiar with constraints). On that note, I propose:
A few other thoughts without real suggestions: I think that the enumeration version of the list comprehension is a bit less readable than the others. (Not saying it’s hard - it just feels a bit less natural, maybe because we’re mixing Rust and Python syntax). Since we’re using python syntax maybe something like Also, with the current enumeration, does this mean we only take as many items as are included in the range? Or would we fail when there’s a mismatched number like in the example |
Beta Was this translation helpful? Give feedback.
-
A few thoughts on using selectors. Frequently, we want to apply some set of constraints only if some condition is true. For example, for Rescue Prime constraints used above, we usually want to apply them only on the first 7 steps of an 8 step cycle. This, of course, could be baked into the constraints themselves, but that would make them much less readable and modular. So, I think some syntax which will help with applying selectors could be very helpful. Here are preliminary sketches on how this could work. Single selectorFirst, if we have just a single selector, we could do something like this:
The above specifies that to all constraints returned from We could also have the compiler check whether Multi selectorWhile we should be able to describe everything using single selectors, frequently it could be much more convenient to specify that one out of several constraints should apply in a given situation. For this, we could use something like that:
The above would translate into the following constraint: We could also have the compiler check whether all The above methodology would also work if evaluators return different number of constraints. For example:
The above would translate to two constraints: And if some constraints are redundant, the compiler can simplify those constraints very easily. For example:
Would translate to something like: Instead of the second constraint being something like: SummaryWith the last several posts, I think we have general solutions for most items. There are still a bunch of details to figure out, but in terms of big questions, I think there are two left:
|
Beta Was this translation helpful? Give feedback.
-
One way to handle boundary constraints and in general to define the main file for an AIR for a given computation could be as follows:
There are a few issues with the above - so, maybe something else would work better, or we can find a way to improve this:
|
Beta Was this translation helpful? Give feedback.
-
Based on discussions with @grjte, we can start the implementation by building different components of the constraint description language for one of the simpler constraints that we have defined in rust for now. Here are the constraints for bitwise chiplet defined in the constraint description language based on the syntax proposed by @bobbinth and others above. |
Beta Was this translation helpful? Give feedback.
-
As we write more and more constraints, it becomes clear that some tools to help with this task are needed. Our current approach is to write constraints directly in Rust. This creates a few challenges:
It would be really nice if we had a tool which could be used to write constraints in human-readable form (or as close to this as possible), and then this tool would perform algebraic optimizations (e.g., to minimize number of multiplications) and output very performant Rust code.
Some of my current thoughts on potential properties of this tool:
a. Transition constraints (over two consecutive rows).
b. Boundary constraints (at least on the first and the last rows).
c. Multiset checks (using just one additional trace segment).
d. Periodic columns.
a. We should be able to write constraints in a modular way. For example, one module for memory constraints, another module for decoder constraints etc.
b. The tool should be able to infer degrees of transition constraints automatically.
c. It would be really nice if the tool could output code for prover and verifier separately - so that it would be possible to take advantage of optimizations only possible in the base field. Though, maybe this could be a feature which gets added later on.
Below are some sketches of how describing constraints could look like.
The above describes a subset of memory co-processor constraints. Most of the things should be self-explanatory, but to give a few clarifications:
let
keyword defines a variable,enf
keyword enforces a constraintctx
is value in the current row of columnctx
, butctx'
is the value in the next row.Here is another example to show how modular approach could work:
In the above,
hasher
,bitwise
, andmemory
are separate modules.Beta Was this translation helpful? Give feedback.
All reactions