Dynamic Lookups
In the last section, we implemented a static lookup. A dynamic lookup is the same as a static lookup except that the values that are being looked up are not known before the proving process (i.e. they are not preprocessed columns but trace columns).
In this section, we will implement one of the simplest dynamic lookups: a permutation check.
A permutation check simply checks that two sets of values have the same elements, but not necessarily in the same order. For example, the values [1, 2, 3]
and [3, 1, 2]
are a permutation of each other, but [1, 2, 3]
and [1, 2]
are not.
If you went through the previous section, you should have a good intuition for how to implement this. First create two LogUp columns where the first column contains the values in the original set of values with multiplicity \(1\) and the second column contains the values in the second set of values with multiplicity \(-1\). Then, check that the claimed_sum
, or the sum of the fractions in the two LogUp columns, is \(0\).
We can optimize further by batching the two columns into a single LogUp column so that a LogUp column row looks something like \(\frac{1}{col_1} - \frac{1}{col_2}\).
fn gen_trace(log_size: u32) -> Vec<CircleEvaluation<SimdBackend, M31, BitReversedOrder>> {
// Create a table with values [0, 1 << log_size)
let mut rng = rand::thread_rng();
let values = (0..(1 << log_size)).map(|i| i).collect::<Vec<_>>();
let original_col = BaseColumn::from_iter(values.iter().map(|v| M31::from(*v)));
// Create a random permutation of the values
let mut permutation = values.clone();
permutation.shuffle(&mut rng);
let permuted_col = BaseColumn::from_iter(permutation.iter().map(|v| M31::from(*v)));
// Convert table to trace polynomials
let domain = CanonicCoset::new(log_size).circle_domain();
vec![original_col, permuted_col]
.into_iter()
.map(|col| CircleEvaluation::new(domain, col))
.collect()
}
fn gen_logup_trace(
log_size: u32,
original_col: &BaseColumn,
permuted_col: &BaseColumn,
lookup_elements: &LookupElements,
) -> (
Vec<CircleEvaluation<SimdBackend, M31, BitReversedOrder>>,
SecureField,
) {
let mut logup_gen = LogupTraceGenerator::new(log_size);
let mut col_gen = logup_gen.new_col();
for row in 0..(1 << (log_size - LOG_N_LANES)) {
// 1 / original - 1 / permuted = (permuted - original) / (original * permuted)
let original_val: PackedSecureField = lookup_elements.combine(&[original_col.data[row]]);
let permuted_val: PackedSecureField = lookup_elements.combine(&[permuted_col.data[row]]);
col_gen.write_frac(
row,
permuted_val - original_val,
original_val * permuted_val,
);
}
col_gen.finalize_col();
logup_gen.finalize_last()
}
fn main() {
...
// Create and commit to the trace columns
let trace = gen_trace(log_size);
let mut tree_builder = commitment_scheme.tree_builder();
tree_builder.extend_evals(trace.clone());
tree_builder.commit(channel);
// Draw random elements to use when creating the random linear combination of lookup values in the LogUp columns
let lookup_elements = LookupElements::draw(channel);
// Create and commit to the LogUp columns
let (logup_cols, claimed_sum) =
gen_logup_trace(log_size, &trace[0], &trace[1], &lookup_elements);
let mut tree_builder = commitment_scheme.tree_builder();
tree_builder.extend_evals(logup_cols);
tree_builder.commit(channel);
...
}
Looking at the code above, we can see that it looks very similar to the implementation in the previous section. Instead of creating a preprocessed column, we create a trace column that contains the values [0, 1 << log_size)
in order. Then, we create a random permutation of the trace column values and set it as the second trace column. Note that this is equivalent to "looking up" all values in the first trace column once. And since all the values are looked up only once, we do not need a separate multiplicity column.
Then, we create a LogUp column that contains the values \(\frac{1}{original} - \frac{1}{permuted}\).
struct TestEval {
log_size: u32,
lookup_elements: LookupElements,
}
impl FrameworkEval for TestEval {
fn log_size(&self) -> u32 {
self.log_size
}
fn max_constraint_log_degree_bound(&self) -> u32 {
self.log_size + CONSTRAINT_EVAL_BLOWUP_FACTOR
}
fn evaluate<E: EvalAtRow>(&self, mut eval: E) -> E {
let original_col = eval.next_trace_mask();
let permuted_col = eval.next_trace_mask();
eval.add_to_relation(RelationEntry::new(
&self.lookup_elements,
E::EF::one(),
&[original_col],
));
eval.add_to_relation(RelationEntry::new(
&self.lookup_elements,
-E::EF::one(),
&[permuted_col],
));
eval.finalize_logup_in_pairs();
eval
}
}
The TestEval
struct is also very similar to the one in the previous section. The only difference is that we call add_to_relation
twice and add them together by calling finalize_logup_in_pairs()
on the TestEval
instance. This is equivalent to calling the finalize_logup_batched
function with &vec![0, 0]
.