-
Notifications
You must be signed in to change notification settings - Fork 158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
LogUp-based Virtual Bus #1307
LogUp-based Virtual Bus #1307
Conversation
let poly_a = circuit.p_0_vec[0].to_owned(); | ||
let poly_b = circuit.p_1_vec[0].to_owned(); | ||
let poly_c = circuit.q_0_vec[0].to_owned(); | ||
let poly_d = circuit.q_1_vec[0].to_owned(); | ||
let mut merged_mls = vec![poly_a, poly_b, poly_c, poly_d, poly_x]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be changed
// construct the vector of multi-linear polynomials | ||
// TODO: avoid unnecessary allocation | ||
let poly_a = circuit.p_0_vec[layer_id].to_owned(); | ||
let poly_b = circuit.p_1_vec[layer_id].to_owned(); | ||
let poly_c = circuit.q_0_vec[layer_id].to_owned(); | ||
let poly_d = circuit.q_1_vec[layer_id].to_owned(); | ||
let mut mls = vec![poly_a, poly_b, poly_c, poly_d, poly_x]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be changed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, putting the EqFunction
as the last ml
is very brittle, and makes the code harder to follow. Ultimately, we need the EqFunction
evaluation in the GkrComposition::evaluate()
; couldn't we just evaluate it there directly?
And then SimpleGkrFinalClaimBuilder
wouldn't need to remove the last opening.
for i in 0..num_evaluations { | ||
for j in 0..4 { | ||
let query: Vec<E> = mls.iter().map(|ml| ml[i]).collect(); | ||
|
||
composition_polys[j].iter().for_each(|c| { | ||
let evaluation = c.as_ref().evaluate(&query); | ||
num_den[j].push(evaluation); | ||
}); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should use multi-threads
8e2173e
to
1bd16e1
Compare
// on memory usage. | ||
let trace_len = trace.num_rows(); | ||
let mut mls: Vec<MultiLinearPoly<E>> = trace | ||
.columns() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only the columns which figure in the virtual bus relation should be used here. At the moment all of the trace is used but this should changed in the future
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this - it will really help to clear up any remaining confusions for me!
(Not a full review yet)
In addition to the main suggestion I made in the last comment, I'm confused about our use of CompositionPolynomial
and MultiLinearPoly
. We make a MultiLinearPoly
for each column in our trace, and then use CompositionPolynomial
s to encode looking up these columns to build the numerators and denominators. This confuses me, since MultiLinearPoly
is supposed to be the
I haven't read through the entire PR, but could we do away with CompositionPolynomial
s? It seems like they come with a lot of boilerplate (a different type for each numerator/denominator expression, use of Arc<dyn ...>
, etc), and are usually little more than accessing 1-3 trace columns. Basically we could make MultiLinearPoly
actually represent the f_i
s, and then use functions to represent the various numerator/denominators (haven't thought too much exactly how to best model this yet)?
.columns() | ||
.map(|col| { | ||
let mut values: Vec<E> = col.iter().map(|value| E::from(*value)).collect(); | ||
values[trace_len - 1] = E::ZERO; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we zero out the last row?
Also we could use the slice last_mut()
method instead of [trace_len - 1]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is because we randomize the last row of the trace here
miden-vm/processor/src/trace/mod.rs
Line 332 in 3e74f9c
column[i] = rng.draw().expect("failed to draw a random value"); |
This is mainly because of some quirks regarding degree validation in Winterfell.
Will change to last_mut()
let outp_p_0 = (0..len / 2) | ||
.map(|i| inp_p_0[i] * inp_q_1[i] + inp_p_1[i] * inp_q_0[i]) | ||
.collect::<Vec<E>>(); | ||
let outp_p_1 = (len / 2..len) | ||
.map(|i| inp_p_0[i] * inp_q_1[i] + inp_p_1[i] * inp_q_0[i]) | ||
.collect::<Vec<E>>(); | ||
let outp_q_0 = (0..len / 2).map(|i| inp_q_0[i] * inp_q_1[i]).collect::<Vec<E>>(); | ||
let outp_q_1 = (len / 2..len).map(|i| inp_q_0[i] * inp_q_1[i]).collect::<Vec<E>>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This yields a "tangled" circuit, which is to draw out and reason about. If we view each gate of the circuit as performing a sum over projective coordinates (p0, q0) + (p1, q1)
(the interpretation in the paper)
out_p_0
is the vector of allp0
s of the next layer (gates' left numerator)out_p_1
is the vector of allp1
s of the next layer (gates' right numerator)out_q_0
is the vector of allq0
s of the next layer (gates' left denom)out_q_1
is the vector of allq1
s of the next layer (gates' right denom)
However, out_p_1
is defined to start at index len/2
, i.e. in the next layer, the first gate's right numerator is halfway down the first layer. I would have expected out_p_1[0]
to be made up of indices 2 and 3 from the first layer instead.
But more generally, I find keeping track of the p's and q's like this very difficult. It would seem more natural to me to have some ProjectiveCoordinate
type, with addition defined (p0, q0) + (p1, q1) = (p0*q1 + p1*q0, q0*q1)
, and encode the circuit as a list of ProjectiveCoordinates
. Then, to get the next layer, we just apply the sum to neighbors: [layer[0] + layer[1], layer[2] + layer[3], ...]
, where layer
is a vector of all projective coordinates for a given layer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To describe what is happening in the code above, we can do the following instead of lines 121-126
let mut out_p = (0..len)
.map(|i| inp_p_0[i] * inp_q_1[i] + inp_p_1[i] * inp_q_0[i])
.collect::<Vec<E>>();
let outp_p_1 = out_p.split_off(len/2);
let outp_p_0 = out_p;
The use of "projective coordinate" in the paper is just to give some intuition on the technique. However, and this is why the code avoids its mention or use, the numerator (or denominators) should be viewed as multi-linear extension polynomials (especially for the deeper layers). With this view, if we denote by *_p_0
and *_p_1
vector. This translates directly to the formulas given in the paper for the sum-checks at a given layer.
If you still think that the use of projective coordinates is more intuitive, I would suggest that we create an issue for it and tackle it once we are decided on the bigger issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I understand it, this doesn't yield the same circuit as in the paper. IIUC, it should be:
let mut out_p_0 = Vec::new();
let mut out_p_1 = Vec::new();
let mut out_q_0 = Vec::new();
let mut out_q_1 = Vec::new();
for i in 0..len/2 {
out_p_0[i] = in_p_0[2*i] * in_q_1[2*i] + in_p_1[2*i] * in_q_0[2*i];
out_p_1[i] = in_p_0[2*i + 1] * in_q_1[2*i + 1] + in_p_1[2*i + 1] * in_q_0[2*i + 1];
out_q_0[i] = in_q_0[2*i] * in_q_1[2*i];
out_q_1[i] = in_q_0[2*i + 1] * in_q_1[2*i + 1];
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Upon closer inspection, I see that your implementation is consistent with how you defined the input layer here. However, I would change that too, so that it corresponds to expanding the LogUp sum for the range checker bus. Specifically, the sum expression is:
Expanded out, we get
Given the tree we would build with these as the leaves, I would expect our 4 numerator/denominators to be defined as:
compared to how they are currently defined:
I would love to simplify this also. Indeed, the use of so much boilerplate is a bit irritating. I have spent some time thinking about it and I think that there might be a simpler way to do things. Will update this comment soon... |
// generate challenge to batch two sumchecks | ||
let data = vec![claim.0, claim.1]; | ||
transcript.reseed(H::hash_elements(&data)); | ||
let r_batch = transcript.draw().map_err(|_| ProverError::FailedToGenerateChallenge)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could make
pub enum ProverError {
// ...
FailedToGenerateMl(#[from] RandomCoinError)
// ...
}
and change this line to
let r_batch = transcript.draw()?;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be nice. I tried it and I think we need to modify RandomCoinError
as well. Here is the error:
missing #[error("...")] display attribute
let p1 = proof.openings_claim.openings[1]; | ||
let q0 = proof.openings_claim.openings[2]; | ||
let q1 = proof.openings_claim.openings[3]; | ||
claim = (p0 + r_layer * (p1 - p0), q0 + r_layer * (q1 - q0)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why claims at the GKR level are not reduced the same way as the sum-check level? Given 2 claims c0
and c1
, and some randomness r
,
- GKR:
c = c0 + r * (c1 - c0)
- sum-check:
c = c0 + r * c1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason for the expression used in the GKR claim reduction is due to the use of linear interpolation formula to evaluate a multi-linear.
We could use the same for batching the two sum-checks but we don't need to afais. There is a slight advantage of having a subtraction less in the currently used batching expression.
d1dd2eb
to
a63612e
Compare
for i in 1..num_rounds { | ||
// generate random challenge r_i for the i-th round | ||
let round_challenge = coin.draw().map_err(|_| Error::FailedToGenerateChallenge)?; | ||
|
||
// compute the new reduced round claim | ||
let new_round_claim = | ||
reduce_claim(&round_proofs[i - 1], current_round_claim, round_challenge); | ||
|
||
// fold each multi-linear using the round challenge | ||
mls.iter_mut().for_each(|ml| ml.bind(round_challenge)); | ||
|
||
// run the i-th round of the protocol using the folded multi-linears for the new reduced | ||
// claim. This basically computes the s_i polynomial. | ||
let round_poly_evals = sumcheck_round(&self.composition_poly, mls); | ||
|
||
// update the claim | ||
current_round_claim = new_round_claim; | ||
|
||
let round_poly_coefs = round_poly_evals.to_poly(current_round_claim.claim); | ||
|
||
// reseed with the s_i polynomial | ||
coin.reseed(H::hash_elements(&round_poly_coefs.coefficients)); | ||
let round_proof = RoundProof { round_poly_coefs }; | ||
round_proofs.push(round_proof); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we introduce new_round_claim
variable here? Seems like the following should be equivalent:
for i in 1..num_rounds {
// generate random challenge r_i for the i-th round
let round_challenge = coin.draw().map_err(|_| Error::FailedToGenerateChallenge)?;
// compute the new reduced round claim
current_round_claim =
reduce_claim(&round_proofs[i - 1], current_round_claim, round_challenge);
// fold each multi-linear using the round challenge
mls.iter_mut().for_each(|ml| ml.bind(round_challenge));
// run the i-th round of the protocol using the folded multi-linears for the new reduced
// claim. This basically computes the s_i polynomial.
let round_poly_evals = sumcheck_round(&self.composition_poly, mls);
let round_poly_coefs = round_poly_evals.to_poly(current_round_claim.claim);
// reseed with the s_i polynomial
coin.reseed(H::hash_elements(&round_poly_coefs.coefficients));
let round_proof = RoundProof { round_poly_coefs };
round_proofs.push(round_proof);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's mainly for readability. No strong preference from my side though
fn sumcheck_round<E: FieldElement>( | ||
composition_poly: &dyn CompositionPolynomial<E>, | ||
mls: &mut [MultiLinearPoly<E>], | ||
) -> UnivariatePolyEvals<E> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few questions here:
- Why do we need dynamic objects here (i.e.,
dyn
)? Could we not just use simple generics? - It seems like invocation of
sumcheck_round()
is always followed by.to_poly()
call on the result. Would it make sense to move.to_poly()
call into thesumcheck_round()
function. - Why
mls
parameter needs to be passed in asmut
? It doesn't seem like we are mutating it in the function.
Applying all 3 changes above, the signature of the function could look like so:
fn sumcheck_round<E: FieldElement, P: CompositionPolynomial<E>>(
composition_poly: &P,
mls: &[MultiLinearPoly<E>],
) -> UnivariatePolyCoef<E>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Probably a remnant from a previous approach. Changed.
- We can, but then we would have to pass
claim
so that we can callto_poly
. I think keepingsumcheck_round
clean is a good idea but, again, no strong preference from my side. - I doesn't. Changed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2. We can, but then we would have to pass
claim
so that we can callto_poly
. I think keepingsumcheck_round
clean is a good idea but, again, no strong preference from my side.
Ah - I see. Let's keep it as is for now.
/// A sum-check round proof. | ||
/// | ||
/// This represents the partial polynomial sent by the Prover during one of the rounds of the | ||
/// sum-check protocol. The polynomial is in coefficient form and excludes the coefficient for | ||
/// the linear term as the Verifier can recover it from the other coefficients and the current | ||
/// (reduced) claim. | ||
#[derive(Debug, Clone)] | ||
pub struct RoundProof<E: FieldElement> { | ||
pub round_poly_coefs: UnivariatePolyCoef<E>, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like RoundProof
and UnivariatePolyCoef
are basically the same thing and are always used together. I'm wondering if we need two separate structs for it. Alternatives could be:
- Keep
RoundProof
and get rid ofUnivariatePolyCoef
. To do this, we'll need to moveevaluate_using_claim()
method into theRoundProof
struct. - Keep
UnivariatePolyCoef
and get rid ofRoundProof
- this would take even fewer changes than option 1 above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another option (especially if we go with the changes to the sumcheck_round()
function described in #1307 (comment)) could be to get rid of RoundProof
and UnivariatePolyEvals
and keep just UnivariatePolyCoef
(and maybe rename it to just UnivariatePoly
). This struct then could look like this:
pub struct UnivariatePoly<E> {
coefficients: Vec<E>,
}
impl<E: FieldElement> UnivariatePoly<E> {
pub fn from_evaluations(evaluations: Vec<E>, round_claim: E) -> Self {
// this would be basically the same method as UnivariatePolyEvals::to_poly()
}
pub fn evaluate_using_claim(&self, claim: E, challenge: E) -> E {
// this method would be the same as now
}
pub fn hash<H: ElementHasher<BaseField = E::BaseField>>(&self) -> H::Digest {
// compute the hash of coefficients
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think keeping RoundProof
is nice from a clarity point of view but on the other hand it is no different than a polynomial for all practical purposes.
If we are willing to pass claim
to sumcheck_round
then the above should work
refactor: serialize usize with write_usize (#1266)
* change prover * change verifier * cleanup * Remove `VirtualBusVerifier` * Remove `VirtualBusProver` * reorganize exports * `LayerGatesInputs` * use constant * docstrings * add TODO * remove TODO * clippy * add static_assertions * add padding comment * add back `claim` in verifier
b4468a1
to
173e26a
Compare
* `ProjectiveCoordinates` * Refactor `LayerGatesInputs` * FractionalSumCircuit2 WIP * Use `FractionalSumCircuit2` in prover * SumCheckProverError * fix number of layers computed * SumCheckVerifierError * Remove old `FractionalSumCircuit` * Remove old `LayerGatesInputs` * Remove `CircuitGateInputs` * Document and rename * docs * Document `ProjectiveCoordinates` * sumcheck_round variable ordering * fix direction of sum-check * Simplify and clean up (#1347) * wip: change terminology of circuit eval * wip: remove left/right num/denom * chore: remove static * chore: remove old file and comments * Document `project_lower_variable` * chore: minor nits and renaming --------- Co-authored-by: Philippe Laferriere <plafer@protonmail.com> * add TODOP * Use `Wire` terminology * Remove `CompositionPolynomial::num_variables()` * fix capacity * rename `NUM_WIRES_PER_TRACE_ROW` constant * TODOP * Introduce `reduce_layer_claim` * use left/right terminology in GkrCompositionMerge * adjust TODOP * fix docs * docs * clippy * clippy * cleanup prove_before_final_circuit_layers loop No longer uses an index * adjust docs * fmt * doc nit * nodes -> wires * doc nit * cleanup doc * document loop * fmt --------- Co-authored-by: Al-Kindi-0 <82364884+Al-Kindi-0@users.noreply.github.com>
Note: changed the base to |
miden-air = { package = "miden-air", path = "../air", version = "0.9", default-features = false } | ||
static_assertions = "1.1.0" | ||
thiserror = { version = "1.0", default-features = false } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we using this somewhere? I think standard thiserror
requires stdblib
and we should probably use the one forked by @bitwalker as we do in the assembler. Though, we should probably move it into this org - probably as miden-thiserror
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah we should use the fork anywhere in this workspace that we depend on thiserror
, at least temporarily - the error_in_core
feature is stabilized in 1.80, and I expect that the mainline thiserror
will support no-std shortly thereafter. With that in mind, it probably is not worth moving the thiserror
fork to our org for such a short time. That said, I'll double check and see what the plan is though for thiserror
, and if it looks like it'll be a month or two, then moving it makes more sense (I don't actually care whether we move it now or not, but didn't want to overcomplicate things for what amounts to a temporary workaround).
We probably should move the miette
fork to our org though, since that will likely stick around for awhile longer, even once the thiserror
fork goes away. I can try to find some time to do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the
error_in_core
feature is stabilized in 1.80
Ah - I didn't realize it is already slated for 1.80. In this case, no need to create miden-thiserror
- we can just rely on your fork until the canonical thiserror
is updated.
/// Converts a main trace row (or more generally "query") to numerators and denominators of the | ||
/// input layer. | ||
fn evaluate_fractions_at_main_trace_query<E>( | ||
query: &[E], | ||
log_up_randomness: &[E], | ||
) -> [[E; NUM_WIRES_PER_TRACE_ROW]; 2] | ||
where | ||
E: FieldElement, | ||
{ | ||
// numerators | ||
let multiplicity = query[M_COL_IDX]; | ||
let f_m = { | ||
let mem_selec0 = query[CHIPLETS_OFFSET]; | ||
let mem_selec1 = query[CHIPLETS_OFFSET + 1]; | ||
let mem_selec2 = query[CHIPLETS_OFFSET + 2]; | ||
mem_selec0 * mem_selec1 * (E::ONE - mem_selec2) | ||
}; | ||
|
||
let f_rc = { | ||
let op_bit_4 = query[DECODER_OP_BITS_OFFSET + 4]; | ||
let op_bit_5 = query[DECODER_OP_BITS_OFFSET + 5]; | ||
let op_bit_6 = query[DECODER_OP_BITS_OFFSET + 6]; | ||
|
||
(E::ONE - op_bit_4) * (E::ONE - op_bit_5) * op_bit_6 | ||
}; | ||
|
||
// denominators | ||
let alphas = log_up_randomness; | ||
|
||
let table_denom = alphas[0] - query[V_COL_IDX]; | ||
let memory_denom_0 = -(alphas[0] - query[MEMORY_D0_COL_IDX]); | ||
let memory_denom_1 = -(alphas[0] - query[MEMORY_D1_COL_IDX]); | ||
let stack_value_denom_0 = -(alphas[0] - query[DECODER_USER_OP_HELPERS_OFFSET]); | ||
let stack_value_denom_1 = -(alphas[0] - query[DECODER_USER_OP_HELPERS_OFFSET + 1]); | ||
let stack_value_denom_2 = -(alphas[0] - query[DECODER_USER_OP_HELPERS_OFFSET + 2]); | ||
let stack_value_denom_3 = -(alphas[0] - query[DECODER_USER_OP_HELPERS_OFFSET + 3]); | ||
|
||
[ | ||
[multiplicity, f_m, f_m, f_rc, f_rc, f_rc, f_rc, E::ZERO], | ||
[ | ||
table_denom, | ||
memory_denom_0, | ||
memory_denom_1, | ||
stack_value_denom_0, | ||
stack_value_denom_1, | ||
stack_value_denom_2, | ||
stack_value_denom_3, | ||
E::ONE, | ||
], | ||
] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this basically the only place where we have logic specific to Miden VM? Not for this PR, but I'm wondering if everything else is generic logic that could be a part of Winterfell rather than Miden VM. For example, we could introduce a method on the Air
trait that would basically do the job of this function and look something like this:
fn build_gkr_input_layer<F, E>(&self, row: &[F], logup_rands: &[E) -> [Vec<E>; 2]
where
F: FieldElement<BaseField = Self::BaseField>,
E: FieldElement<BaseField = Self::BaseField> + ExtensionOf<F>,
And then both GKR prover and verifier would be able to use this to build and verify the proof respectively.
A couple of other questions:
- What is
log_up_randomness
here? Is it a set of random values generated from the main trace commitment root? Are these the "lambdas" referenced in this issue or is it something else?- I'm assuming we are using only the first element from
log_up_randomness
because of the nature of the constraints. If we had more complicated tables (e.g., where multiple columns needed to be merged into a single value per row), we'd need to use more random elements - right?
- I'm assuming we are using only the first element from
- Where would we specify "boundary" constraints for the buses? Specifically, for sub-busses which do not start and/or end with 0, what would be the way to specify initial/final values?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that makes sense. This will require however that Winterfell is able to handle the logic for building the s
-column similar to how it does for the l
-column (i.e., Lagrange column).
To this end we would need to introduce the concept of an oracle. This is basically a wrapper around CompositionPolynomial
with information about the index
of the columns appearing in the algebraic expression encoded in the CompositionPolynomial
as well as additional information needed to verify the final evaluation claim e.g., whether a column is periodic (or more generally transparent) and thus can be computed by the verifier un-assisted and hence does not need to be included in the s
-column, whether a column is a shift as this is important when building the s
-column...
- What is
log_up_randomness
here? Is it a set of random values generated from the main trace commitment root?
Yes, these are the randomness generated from the main trace commitment, usually called the
Where would we specify "boundary" constraints for the buses? Specifically, for sub-busses which do not start and/or end with 0, what would be the way to specify initial/final values?
The verifier will compute all of the boundary values in order to get a single claim value. The verifier will then check that the output of the LogUp circuit equals that claim. More precisely this would be done here
pub fn verify<
E: FieldElement<BaseField = Felt>,
C: RandomCoin<Hasher = H, BaseField = Felt>,
H: ElementHasher<BaseField = Felt>,
>(
claim: E,
proof: GkrCircuitProof<E>,
log_up_randomness: Vec<E>,
transcript: &mut C,
) -> Result<FinalOpeningClaim<E>, VerifierError> {
...
// check that the output matches the expected `claim`
if (p0 * q1 + p1 * q0) / (q0 * q1) != claim {
return Err(VerifierError::MismatchingCircuitOutput);
}
...
}
At the moment, claim
is set to zero but if we want to go with the proposal of delegating everything to Winterfell, then we would need to add a method to the GKR verifier so that it can compute claim
from the public inputs.
/// A GKR proof for the correct evaluation of the sum of fractions circuit. | ||
#[derive(Debug)] | ||
pub struct GkrCircuitProof<E: FieldElement> { | ||
circuit_outputs: CircuitLayerPolys<E>, | ||
before_final_layer_proofs: BeforeFinalLayerProof<E>, | ||
final_layer_proof: FinalLayerProof<E>, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: how big can we expect the proof to be (e.g., in KB) and what are the primary drivers? I'm guessing the primary driver is the complexity of the bus constraints - but does trace length also make a big difference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The proof is composed of two parts:
- The round polynomials: The number of these is dependent on the trace length as well as the complexity of the bus. The degree of these is dependent on the max selector degree.
- The opening points: The number of this dependent on the trace length and the complexity of the bus.
To give a concrete formula, suppose that the trace length is
- The number of field elements defining the round polynomials is given by
$$\frac{3\cdot(\mu + \nu - 1)\cdot(\mu + \nu)}{2} + 3\cdot\mu + d\cdot\nu,$$ the$3$ represents the degree of the round polynomials in the sum-checks appearing in all but the final sum-check, while the$d$ represent the degree of the round polynomial for the final sum-check (which depends as stated above on the degree of the selectors). - The number of opening points is given by
$$4\cdot (\mu + \nu - 1) + k,$$ where$k$ is the number of trace columns, as well as their shifts, appearing in the bus.
Note that all elements in the above are extension field elements.
To give some concrete numbers, suppose that
- The proof size due to round polynomials is:
1015
extension field elements which translates in the case of a quadratic extension degree to about16Kb
. - The proof size due to opening points is:
136
extension field elements assuming we open 40 columns and column-shifts. This translates in the case of a quadratic extension degree to about2Kb
.
/// defined from the main trace columns. | ||
pub fn new( | ||
main_trace_columns: &[MultiLinearPoly<E>], | ||
log_up_randomness: &[E], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
log_up_randomness: &[E], | |
log_up_randomness: &E, |
log_up_randomness
is guaranteed to be a single element, so we should change it everywhere to only be &E
Describe your changes
An initial version of the GKR prover for the LogUp-based global virtual bus for Miden VM.
A few remarks:
.clone()
instances that could and should be avoided.A note about the usage in the context of the overall STARK. After the prover commits to the main-trace and sends the commitment to the verifier, the verifier replies with the LogUp randomness (i.e., the$\alpha$ values). At this point, and as part of the auxiliary trace building logic (see here), the prover initializes and runs
The above
gkr_proof
should be attached to the usual STARK proof so that the STARK verifier can call a GKR verifier to verify it as part of its proof verification.As part of the auxiliary trace building logic still, the prover then uses
To get the randomness defining the Lagrange kernel auxiliary trace column and the openings which will be used to define a boundary constraint against the multi-linear opening helper column (column$\lambda$ as
s
here) and before that the batching randomnessAt this point the constraints, both boundary and transition, are updated and both multi-linear opening helper columns of the auxiliary trace are constructed. This concludes the auxiliary trace building stage and at this point the protocol continues as before.
Checklist before requesting a review
next
according to naming convention.