-
Notifications
You must be signed in to change notification settings - Fork 132
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
feat: add assertion for dynamic lookup #347
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #347 +/- ##
==========================================
+ Coverage 81.89% 82.68% +0.79%
==========================================
Files 82 83 +1
Lines 17010 17955 +945
==========================================
+ Hits 13930 14846 +916
- Misses 3080 3109 +29 ☔ View full report in Codecov by Sentry. |
// this check ensures that one dedicated `Fixed` column or `Selector` is used | ||
// for enabling the real table rows of the column, which is used as `TableColumn` role. | ||
// otherwise, we get the soundness error, like https://github.com/privacy-scaling-explorations/halo2/issues/335 | ||
if table.degree() < 2 { |
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.
sorry if im wrong, but i don't understand how the degree should be more than 2?
shouldn't it be the opposite? we want it to be <2
pub fn degree(&self) -> usize { |
you say in the comment that you expect the column to be "Fixed column or Selector" but I thought the purpose of lookup_any
was to allow a lookup into Advice column. If we're looking up into a Fixed column, we might as well use regular lookup()
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.
shouldn't it be the opposite?
No, the degree of table expression
should be 2.
In other words, it should be like following:
meta.lookup_any(|cells| {
let default = Expression::constant(F::ONE);
let s_lookup = meta.query_selector(...);
let to_lookup = meta.query_advice(...);
let s_ltable = meta.query_selector(...);
let ltable = meta.query_advice(...);
vec![
(
s_lookup * to_lookup + (Expression::constant(F::ONE) - s_lookup) * default,
s_ltable * ltable
)
]
}
In the example, the degree of s_ltable * ltable
is 2. Since the degree of selector
& advice
column is 1, and the degree of its product is sum of degrees.
you say in the comment that you expect the column to be "Fixed column or Selector" but I thought the purpose of lookup_any was to allow a lookup into Advice column.
I meant that extra fixed column or selector SHOULD be accompanied with table column(in lookup_any
, the advice column).
In other words, when using lookup_any
, the advice/fixed column used for table role should be accompanied with another fixed column or selector to indicate which rows are used as table rows.
halo2_frontend/src/dev.rs
Outdated
// `Selector` is required to be enabled, in order to indicate the lookup table values. | ||
// | ||
// The following diagram shows the example circuit. | ||
// (This is NOT a real `FaultyCircuit` instantiation we have in this test. Just borrow of its config.) |
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'm a bit confused by this statement. Could you clarify this?
Also I'm a bit confused about this test 😅, it assigns two regions, and the table is assigned twice (once per region). I think it would be more clear with 3 regions:
- 1 region to assign the table once
- 1 region to enable
q
and assign the inputsa
: "good" - 1 region to enable
q
without assignment ofa
: "faulty"
Anyway, I guess you updated this test so that the the new failure check doesn't trigger, right?
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.
Thanks for letting me know! @ed255
I'm a bit confused by this statement. Could you clarify this?
Yes, I see that the new test and example circuit explanation confuses people. 😅
I will improve the statement.
Anyway, I guess you updated this test so that the the new failure check doesn't trigger, right?
Yes, I updated the test so that it does lead to wrong assignment error, not the lookup_any
check error.
// this check ensures that one extra, dedicated `Fixed` column or `Selector` is used | ||
// for enabling the real table rows of the column, which is used as `TableColumn`. | ||
// otherwise, we get the soundness error, like https://github.com/privacy-scaling-explorations/halo2/issues/335 | ||
if table.degree() < 2 { |
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 this check is not effective in certain scenarios.
- For example, let's say you build a table expression by multiplying two advice queries. The degree is 2, but it's still problematic. This would be a false negative.
- Imagine you have a dynamic table that combines fixed and advice columns. In that case it can be OK for some table expressions to be degree 1. This would be a false positive.
As an example of the second case, take a look at the block_table
from the zkEVM CE project: https://github.com/privacy-scaling-explorations/zkevm-specs/blob/master/specs/tables.md#block_table
The tag
and index
columns are fixed and the valueLo
and valueHi
are advice.
Assume that the tag
values go from 1 to N (and the assignments to the unused rows are 0)
now we want to do a lookup to Time
for example. The input expression would be [q * Time, 0, q * in_value, 0]
and the table expression would be [Time, 0, value, 0]
. This table expression can only match a valid row, because invalid rows have Tag=0 so they are [0, ... ]
, and Time != 0
.
So I think the proper check would be to make sure that at least 1 fixed query exists in some of the table expressions. Then we must forbid that all table expressions have 0 fixed queries.
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, thanks for pointing out specific cases.
Yes, I totally agree with your idea.
Will improve the check.
thanks for the PR @guorong009 . it looks good to me, but it doesn't fix #335 , right? |
How about this patch - c80022a ? @teddav @ed255 |
@@ -42,7 +42,7 @@ struct MyCircuitConfig { | |||
|
|||
// Copy constraints between columns (a, b) and (a, d) | |||
|
|||
// A dynamic lookup: s_lookup * [1, a[0], b[0]] in s_ltable * [1, d[0], c[0]] | |||
// A dynamic lookup: s_lookup * [a[0], b[0]] in s_ltable * [d[0], c[0]] |
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.
Sorry but I think my suggestion here #347 (comment) is not complete.
Take a look at this example based on the updated lookup expressions of this circuit:
offset | s_lookup | a | b | s_ltable | c | d |
---|---|---|---|---|---|---|
0 | 1 | 0 | 0 | 1 | 2 | 4 |
1 | 1 | 2 | 4 | 1 | 3 | 9 |
2 | 0 | 0 | 0 | 0 | 0 | 0 |
At offset 0 we have input 1 * [0, 0] = [0, 0]
which matches the table at offset 2 0 * [0, 0] = [0, 0]
but that's undesirable, because we are matching a table entry that has s_ltable = 0
.
So checking that at least one table expression uses a selector/fixed is not enough. Maybe the correct check is that at least one table expression is a selector/fixed.
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.
Thanks for catch! @ed255
So, I think we reached a point that the lookup_any
api should include the table expression
which is a selector/fixed.
Otherwise, it can be used for soundness error by malicious prover.
Also, from #335 & #347 (comment), we can see that when the single fixed col is used for lookup_any
api, we should recommend user to use the lookup
api.
Then, the summary is that we don't have any way to prevent the abuse of lookup_any
api use, and should remove lookup_any
api. right?
if table.degree() == 1 { | ||
panic!("table expression containing only fixed column supplied to lookup_any argument, should use `lookup` api instead of `lookup_any`"); | ||
} |
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.
Note that this is checking a condition for any table expressions. But actually we want to check that all table expressions are a fixed/selector column query.
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.
Hmm, I think the current checks on lookup_any
api are for all table expressions. Am I wrong?
pub fn lookup_any<S: AsRef<str>>(
&mut self,
name: S,
table_map: impl FnOnce(&mut VirtualCells<'_, F>) -> Vec<(Expression<F>, Expression<F>)>,
) -> usize {
let mut cells = VirtualCells::new(self);
let table_map = table_map(&mut cells)
.into_iter()
.map(|(mut input, mut table)| {
if input.contains_simple_selector() {
panic!("expression containing simple selector supplied to lookup argument");
}
if table.contains_simple_selector() {
panic!("expression containing simple selector supplied to lookup argument");
}
if !table.contains_fixed_col_or_selector() {
panic!(
"table expression supplied to lookup_any argument must include fixed column or selector"
);
}
if table.degree() == 1 {
panic!("table expression containing only fixed column supplied to lookup_any argument, should use `lookup` api instead of `lookup_any`");
}
input.query_cells(&mut cells);
table.query_cells(&mut cells);
(input, table)
})
.collect();
let index = self.lookups.len();
self.lookups
.push(lookup::Argument::new(name.as_ref(), table_map));
index
}
It iterates every input
and table
expressions, and do the check.
So, if any of table
expression do not match the check, it panics.
@ed255 |
@ed255 @teddav
If you read the PR description and its changes(especially, this one), it will be clear what we are missing. Hence, I updated the assertion checks.
Pls re-review the new updates, with mentioned PR. |
@@ -182,7 +182,7 @@ impl<F: Field + From<u64>, const WIDTH_FACTOR: usize> MyCircuit<F, WIDTH_FACTOR> | |||
let d = meta.query_fixed(d, Rotation::cur()); | |||
let lhs = [one.clone(), a, b].map(|c| c * s_lookup.clone()); | |||
let rhs = [one.clone(), d, c].map(|c| c * s_ltable.clone()); | |||
lhs.into_iter().zip(rhs).collect() | |||
zip(lhs, rhs).chain([(s_lookup, s_ltable)]).collect() |
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.
Previously this was:
s_lookup * [1, a, b] in s_ltable * [1, d, c]
<->
[s_lookup*1, s_lookup*a, s_lookup*b] in [s_ltable*1, s_ltable*d, s_ltable*c]
Now it's:
[s_lookup*1, s_lookup*a, s_lookup*b, s_lookup] in [s_ltable*1, s_ltable*d, s_ltable*c, s_ltable]
Isn't the first pair (input_expr[0], table_expr[0])
redundant with the last one (input_expr[3], table_expr[3])
?
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 answer is no. (starting from my understanding) @ed255
I understood the original constraints as lookup of [1, 1], [a, d], [b, c]
.
In other words, [1, 1]
pair is one part of whole lookup, NOT as the pair of selector
s.
At first, I thought this is redundant to lookup constants [1, 1]
, but didn't want to change them.
I just wanted to add the new updates to the circuit, not changing any of original code.
That's why I kept the original constraints, just add the extra pair of selector
s at the end.
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 answer is no. (starting from my understanding) @ed255
I understood the original constraints as lookup of
[1, 1], [a, d], [b, c]
. In other words,[1, 1]
pair is one part of whole lookup, NOT as the pair ofselector
s. At first, I thought this is redundant to lookup constants[1, 1]
, but didn't want to change them.
That would be the case if we had
let lhs = [one.clone(), a, b];
But we actually had
let lhs = [one.clone(), a, b].map(|c| c * s_lookup.clone());
So originally it was [s_lookup*1, s_ltable*1], [s_lookup*a, s_ltable*d], [s_lookup*b, s_ltable*c]
Or I'm missing something?
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.
No, you don't miss anything.
Originally, it was [s_lookup*1, s_ltable*1], [s_lookup*a, s_ltable*d], [s_lookup*b, s_ltable*c]
.
Now, I add the [s_lookup, s_ltable]
at the end.
Even though the expressions share the same value, their meanings are different.
I mean (input_expr[0], table_expr[0]) != (input_expr[3], table_expr[3])
<-> [s_lookup*1, s_ltable*1] != [s_lookup, s_ltable]
The reason is, the goal of newly-added expression pair is to check lookup_activator = table_activator = 1(enabled)
.
On the other hand, the goal of first expression pair is to lookup/check 1 = 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.
if is_all_table_expr_single_fixed { | ||
panic!("all table expressions contain only fixed query, should use `lookup` api instead of `lookup_any`"); | ||
} | ||
if !is_all_table_expr_contain_fixed_or_selector { | ||
panic!("all table expressions need selector/fixed query for tagging"); | ||
} | ||
if !is_tagging_exprs_pair_exists { | ||
panic!("pair of tagging expressions(query of the tag columns or mutiple query combinations) should be included"); | ||
} |
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 propose that the #[cfg(feature = "lookup-any-sanity-checks")]
only applies to these if cases that panic.
The rest of the code doesn't need cfg(...) /
cfg(not(...))`
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.
And I have an extra suggestion. I would add the following comment before this section
NOTE: These checks try to detect unsound cases of lookups and are only active
with the `lookup-any-sanity-checks`. False positives may exist: in some
particular scenarios the lookup can be sound but these checks will not pass,
leading to panics. For those cases you can disable the
`lookup-any-sanity-checks` feature. We will appreciate it if you report false
positives by opening issues on the repository.
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.
Now that there's a feature that can be disabled I have more confidence in moving forward!
I think there are false positives, but that should not be an issue now because the check can be disabled; and maybe we can't do the perfect check.
I've written two "final" small requests, and after that this will look good to me!
if is_all_table_expr_single_fixed { | ||
panic!("all table expressions contain only fixed query, should use `lookup` api instead of `lookup_any`"); | ||
} | ||
if !is_all_table_expr_contain_fixed_or_selector { | ||
panic!("all table expressions need selector/fixed query for tagging"); | ||
} | ||
if !is_tagging_exprs_pair_exists { | ||
panic!("pair of tagging expressions(query of the tag columns or mutiple query combinations) should be included"); | ||
} |
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.
And I have an extra suggestion. I would add the following comment before this section
NOTE: These checks try to detect unsound cases of lookups and are only active
with the `lookup-any-sanity-checks`. False positives may exist: in some
particular scenarios the lookup can be sound but these checks will not pass,
leading to panics. For those cases you can disable the
`lookup-any-sanity-checks` feature. We will appreciate it if you report false
positives by opening issues on the repository.
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.
LGTM! Nice work!
Description
Dynamic lookup, which uses the
lookup_any
, should have the assertion to prevent use ofAdvice/Instance
column without extraFixed
column orSelector
.This PR adds assertion to
lookup_any
api.Related issues
Changes
table_expression
containingfixed
col orselector
inlookup_any
apitable_expression
should NOT be one simplefixed
col, inlookup_any
api