-
Notifications
You must be signed in to change notification settings - Fork 162
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
Changes from all commits
44a031e
fa5bd2e
48116ad
b2b2de5
e2ce938
0bbb515
7078870
df9b110
699cb11
3b2acf5
431b937
2686d65
de541c5
5110915
7537be3
87e3c49
720ac33
cea681a
0fed9cd
cb503d1
5b734d1
fa97e9e
63243a8
a275df4
d6cd197
016bc95
f46ac0c
31609c5
7567a10
37af90b
456ebec
173e26a
4206910
09deed7
8c1842b
8b7b5c6
0dca8f2
ddd7b50
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
use crate::trace::virtual_bus::sum_check::SumCheckProverError; | ||
use crate::trace::virtual_bus::sum_check::SumCheckVerifierError; | ||
|
||
#[derive(Debug, thiserror::Error)] | ||
pub enum ProverError { | ||
#[error("failed to generate multi-linear from the given evaluations")] | ||
FailedToGenerateML, | ||
#[error("failed to generate the sum-check proof")] | ||
FailedToProveSumCheck(#[from] SumCheckProverError), | ||
#[error("failed to generate the random challenge")] | ||
FailedToGenerateChallenge, | ||
} | ||
|
||
#[derive(Debug, thiserror::Error)] | ||
pub enum VerifierError { | ||
#[error("one of the claimed circuit denominators is zero")] | ||
ZeroOutputDenominator, | ||
#[error("the output of the fraction circuit is not equal to the expected value")] | ||
MismatchingCircuitOutput, | ||
#[error("failed to generate the random challenge")] | ||
FailedToGenerateChallenge, | ||
#[error("failed to verify the sum-check proof")] | ||
FailedToVerifySumCheck(#[from] SumCheckVerifierError), | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,290 @@ | ||
use core::ops::Add; | ||
|
||
use crate::trace::virtual_bus::multilinear::EqFunction; | ||
use crate::trace::virtual_bus::sum_check::{CompositionPolynomial, RoundProof}; | ||
use alloc::vec::Vec; | ||
use miden_air::trace::chiplets::{MEMORY_D0_COL_IDX, MEMORY_D1_COL_IDX}; | ||
use miden_air::trace::decoder::{DECODER_OP_BITS_OFFSET, DECODER_USER_OP_HELPERS_OFFSET}; | ||
use miden_air::trace::range::{M_COL_IDX, V_COL_IDX}; | ||
use miden_air::trace::{CHIPLETS_OFFSET, TRACE_WIDTH}; | ||
use prover::CircuitLayerPolys; | ||
use static_assertions::const_assert; | ||
use vm_core::{Felt, FieldElement}; | ||
|
||
mod error; | ||
mod prover; | ||
pub use prover::prove; | ||
|
||
mod verifier; | ||
pub use verifier::verify; | ||
|
||
use super::multilinear::MultiLinearPoly; | ||
use super::sum_check::{FinalOpeningClaim, Proof as SumCheckProof}; | ||
|
||
/// Defines the number of wires in the input layer that are generated from a single main trace row. | ||
const NUM_WIRES_PER_TRACE_ROW: usize = 8; | ||
const_assert!(NUM_WIRES_PER_TRACE_ROW.is_power_of_two()); | ||
|
||
// CIRCUIT WIRE | ||
// ================================================================================================ | ||
|
||
/// Represents a fraction `numerator / denominator` as a pair `(numerator, denominator)`. This is | ||
/// the type for the gates' inputs in [`prover::EvaluatedCircuit`]. | ||
/// | ||
/// Hence, addition is defined in the natural way fractions are added together: `a/b + c/d = (ad + | ||
/// bc) / bd`. | ||
#[derive(Debug, Clone, Copy)] | ||
pub struct CircuitWire<E: FieldElement> { | ||
numerator: E, | ||
denominator: E, | ||
} | ||
|
||
impl<E> CircuitWire<E> | ||
where | ||
E: FieldElement, | ||
{ | ||
/// Creates new projective coordinates from a numerator and a denominator. | ||
pub fn new(numerator: E, denominator: E) -> Self { | ||
assert_ne!(denominator, E::ZERO); | ||
|
||
Self { | ||
numerator, | ||
denominator, | ||
} | ||
} | ||
} | ||
|
||
impl<E> Add for CircuitWire<E> | ||
where | ||
E: FieldElement, | ||
{ | ||
type Output = Self; | ||
|
||
fn add(self, other: Self) -> Self { | ||
let numerator = self.numerator * other.denominator + other.numerator * self.denominator; | ||
let denominator = self.denominator * other.denominator; | ||
|
||
Self::new(numerator, denominator) | ||
} | ||
} | ||
|
||
/// 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, | ||
], | ||
] | ||
} | ||
Comment on lines
+71
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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:
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 To this end we would need to introduce the concept of an oracle. This is basically a wrapper around
Yes, these are the randomness generated from the main trace commitment, usually called the
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, |
||
|
||
/// Computes the wires added to the input layer that come from a given main trace row (or more | ||
/// generally, "query"). | ||
fn compute_input_layer_wires_at_main_trace_query<E>( | ||
query: &[E], | ||
log_up_randomness: &[E], | ||
) -> [CircuitWire<E>; NUM_WIRES_PER_TRACE_ROW] | ||
where | ||
E: FieldElement, | ||
{ | ||
let [numerators, denominators] = | ||
evaluate_fractions_at_main_trace_query(query, log_up_randomness); | ||
let input_gates_values: Vec<CircuitWire<E>> = numerators | ||
.into_iter() | ||
.zip(denominators) | ||
.map(|(numerator, denominator)| CircuitWire::new(numerator, denominator)) | ||
.collect(); | ||
input_gates_values.try_into().unwrap() | ||
} | ||
|
||
/// 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>, | ||
} | ||
Comment on lines
+142
to
+148
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. The proof is composed of two parts:
To give a concrete formula, suppose that the trace length is
Note that all elements in the above are extension field elements. To give some concrete numbers, suppose that
|
||
|
||
impl<E: FieldElement> GkrCircuitProof<E> { | ||
pub fn get_final_opening_claim(&self) -> FinalOpeningClaim<E> { | ||
self.final_layer_proof.after_merge_proof.openings_claim.clone() | ||
} | ||
} | ||
|
||
/// A set of sum-check proofs for all GKR layers but for the input circuit layer. | ||
#[derive(Debug)] | ||
pub struct BeforeFinalLayerProof<E: FieldElement> { | ||
pub proof: Vec<SumCheckProof<E>>, | ||
} | ||
|
||
/// A proof for the input circuit layer i.e., the final layer in the GKR protocol. | ||
#[derive(Debug)] | ||
pub struct FinalLayerProof<E: FieldElement> { | ||
before_merge_proof: Vec<RoundProof<E>>, | ||
after_merge_proof: SumCheckProof<E>, | ||
} | ||
|
||
/// Represents a claim to be proven by a subsequent call to the sum-check protocol. | ||
#[derive(Debug)] | ||
pub struct GkrClaim<E: FieldElement> { | ||
pub evaluation_point: Vec<E>, | ||
pub claimed_evaluation: (E, E), | ||
} | ||
|
||
/// A composition polynomial used in the GKR protocol for all of its sum-checks except the final | ||
/// one. | ||
#[derive(Clone)] | ||
pub struct GkrComposition<E> | ||
where | ||
E: FieldElement<BaseField = Felt>, | ||
{ | ||
pub combining_randomness: E, | ||
} | ||
|
||
impl<E> GkrComposition<E> | ||
where | ||
E: FieldElement<BaseField = Felt>, | ||
{ | ||
pub fn new(combining_randomness: E) -> Self { | ||
Self { | ||
combining_randomness, | ||
} | ||
} | ||
} | ||
|
||
impl<E> CompositionPolynomial<E> for GkrComposition<E> | ||
where | ||
E: FieldElement<BaseField = Felt>, | ||
{ | ||
fn max_degree(&self) -> u32 { | ||
3 | ||
} | ||
|
||
fn evaluate(&self, query: &[E]) -> E { | ||
let eval_left_numerator = query[0]; | ||
let eval_right_numerator = query[1]; | ||
let eval_left_denominator = query[2]; | ||
let eval_right_denominator = query[3]; | ||
let eq_eval = query[4]; | ||
eq_eval | ||
* ((eval_left_numerator * eval_right_denominator | ||
+ eval_right_numerator * eval_left_denominator) | ||
+ eval_left_denominator * eval_right_denominator * self.combining_randomness) | ||
} | ||
} | ||
|
||
/// A composition polynomial used in the GKR protocol for its final sum-check. | ||
#[derive(Clone)] | ||
pub struct GkrCompositionMerge<E> | ||
where | ||
E: FieldElement<BaseField = Felt>, | ||
{ | ||
pub sum_check_combining_randomness: E, | ||
pub tensored_merge_randomness: Vec<E>, | ||
pub log_up_randomness: Vec<E>, | ||
} | ||
|
||
impl<E> GkrCompositionMerge<E> | ||
where | ||
E: FieldElement<BaseField = Felt>, | ||
{ | ||
pub fn new( | ||
combining_randomness: E, | ||
merge_randomness: Vec<E>, | ||
log_up_randomness: Vec<E>, | ||
) -> Self { | ||
let tensored_merge_randomness = | ||
EqFunction::ml_at(merge_randomness.clone()).evaluations().to_vec(); | ||
|
||
Self { | ||
sum_check_combining_randomness: combining_randomness, | ||
tensored_merge_randomness, | ||
log_up_randomness, | ||
} | ||
} | ||
} | ||
|
||
impl<E> CompositionPolynomial<E> for GkrCompositionMerge<E> | ||
where | ||
E: FieldElement<BaseField = Felt>, | ||
{ | ||
fn max_degree(&self) -> u32 { | ||
// Computed as: | ||
// 1 + max(left_numerator_degree + right_denom_degree, right_numerator_degree + | ||
// left_denom_degree) | ||
5 | ||
} | ||
|
||
fn evaluate(&self, query: &[E]) -> E { | ||
let [numerators, denominators] = | ||
evaluate_fractions_at_main_trace_query(query, &self.log_up_randomness); | ||
|
||
let numerators = MultiLinearPoly::from_evaluations(numerators.to_vec()).unwrap(); | ||
let denominators = MultiLinearPoly::from_evaluations(denominators.to_vec()).unwrap(); | ||
|
||
let (left_numerators, right_numerators) = numerators.project_least_significant_variable(); | ||
let (left_denominators, right_denominators) = | ||
denominators.project_least_significant_variable(); | ||
|
||
let eval_left_numerators = | ||
left_numerators.evaluate_with_lagrange_kernel(&self.tensored_merge_randomness); | ||
let eval_right_numerators = | ||
right_numerators.evaluate_with_lagrange_kernel(&self.tensored_merge_randomness); | ||
|
||
let eval_left_denominators = | ||
left_denominators.evaluate_with_lagrange_kernel(&self.tensored_merge_randomness); | ||
let eval_right_denominators = | ||
right_denominators.evaluate_with_lagrange_kernel(&self.tensored_merge_randomness); | ||
|
||
let eq_eval = query[TRACE_WIDTH]; | ||
|
||
eq_eval | ||
* ((eval_left_numerators * eval_right_denominators | ||
+ eval_right_numerators * eval_left_denominators) | ||
+ eval_left_denominators | ||
* eval_right_denominators | ||
* self.sum_check_combining_randomness) | ||
} | ||
} |
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
requiresstdblib
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 asmiden-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 - theerror_in_core
feature is stabilized in 1.80, and I expect that the mainlinethiserror
will support no-std shortly thereafter. With that in mind, it probably is not worth moving thethiserror
fork to our org for such a short time. That said, I'll double check and see what the plan is though forthiserror
, 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 thethiserror
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.
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 canonicalthiserror
is updated.