Merge pull request #431 from zcash/mockprover-improve-lookup-error

Improve `dev::VerifyFailure::Lookup` information
This commit is contained in:
str4d 2021-12-20 22:39:33 +00:00 committed by GitHub
commit 3f53d9f6bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 230 additions and 19 deletions

View File

@ -6,6 +6,9 @@ and this project adheres to Rust's notion of
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- `halo2::dev::LookupFailure` (used in `VerifyFailure::Lookup`)
### Changed
- `halo2::plonk::Error` has been overhauled:
- `Error` now implements `std::fmt::Display` and `std::error::Error`.
@ -19,8 +22,12 @@ and this project adheres to Rust's notion of
underlying `io::Error`.
- `halo2::dev::CircuitLayout::render` now takes `k` as a `u32`, matching the
regular parameter APIs.
- `halo2::dev::VerifyFailure::ConstraintNotSatisfied` now has a `cell_values`
field, storing the values of the cells used in the unsatisfied constraint.
- `halo2::dev::VerifyFailure` has been overhauled:
- `VerifyFailure::ConstraintNotSatisfied` now has a `cell_values` field,
storing the values of the cells used in the unsatisfied constraint.
- The `row` field of `VerifyFailure::Lookup` has been replaced by a `location`
field, which can now indicate whether the location falls within an assigned
region.
- `halo2::plonk::ConstraintSystem::enable_equality` and
`halo2::plonk::ConstraintSystem::query_any` now take `Into<Column<Any>>` instead
of `Column<Any>` as a parameter to avoid excesive `.into()` usage.

View File

@ -1,6 +1,7 @@
//! Tools for developing circuits.
use std::collections::HashMap;
use std::collections::HashSet;
use std::fmt;
use std::iter;
use std::ops::{Add, Mul, Neg, Range};
@ -33,6 +34,29 @@ mod graph;
#[cfg_attr(docsrs, doc(cfg(feature = "dev-graph")))]
pub use graph::{circuit_dot_graph, layout::CircuitLayout};
/// The location at which a particular lookup is not satisfied.
#[derive(Debug, PartialEq)]
pub enum LookupFailure {
/// A location inside a region. This may be due to the intentional use of a lookup
/// (if its inputs are conditional on a complex selector), or an unintentional lookup
/// constraint that overlaps the region (indicating that the lookup's inputs should be
/// made conditional).
InRegion {
/// The region in which the lookup's input expressions were evaluated.
region: metadata::Region,
/// The offset (relative to the start of the region) at which the lookup's input
/// expressions were evaluated.
offset: usize,
},
/// A location outside of a region. This most likely means the input expressions do
/// not correctly constrain a default value that exists in the table when the lookup
/// is not being used.
OutsideRegion {
/// The circuit row on which this lookup is not satisfied.
row: usize,
},
}
/// The reasons why a particular circuit is not satisfied.
#[derive(Debug, PartialEq)]
pub enum VerifyFailure {
@ -69,8 +93,8 @@ pub enum VerifyFailure {
/// the order in which `ConstraintSystem::lookup` is called during
/// `Circuit::configure`.
lookup_index: usize,
/// The row on which this lookup is not satisfied.
row: usize,
/// The location at which the lookup is not satisfied.
location: LookupFailure,
},
/// A permutation did not preserve the original value of a cell.
Permutation {
@ -114,9 +138,19 @@ impl fmt::Display for VerifyFailure {
constraint
)
}
Self::Lookup { lookup_index, row } => {
Self::Lookup {
lookup_index,
location,
} => match location {
LookupFailure::InRegion { region, offset } => write!(
f,
"Lookup {} is not satisfied in {} at offset {}",
lookup_index, region, offset
),
LookupFailure::OutsideRegion { row } => {
write!(f, "Lookup {} is not satisfied on row {}", lookup_index, row)
}
},
Self::Permutation { column, row } => {
write!(
f,
@ -132,8 +166,10 @@ impl fmt::Display for VerifyFailure {
struct Region {
/// The name of the region. Not required to be unique.
name: String,
/// The row that this region starts on, if known.
start: Option<usize>,
/// The columns involved in this region.
columns: HashSet<Column<Any>>,
/// The rows that this region starts and ends on, if known.
rows: Option<(usize, usize)>,
/// The selectors that have been enabled in this region. All other selectors are by
/// construction not enabled.
enabled_selectors: HashMap<Selector, Vec<usize>>,
@ -143,14 +179,20 @@ struct Region {
}
impl Region {
fn update_start(&mut self, row: usize) {
fn update_extent(&mut self, column: Column<Any>, row: usize) {
self.columns.insert(column);
// The region start is the earliest row assigned to.
let mut start = self.start.unwrap_or(row);
// The region end is the latest row assigned to.
let (mut start, mut end) = self.rows.unwrap_or((row, row));
if row < start {
// The first row assigned was not at start 0 within the region.
start = row;
}
self.start = Some(start);
if row > end {
end = row;
}
self.rows = Some((start, end));
}
}
@ -385,7 +427,8 @@ impl<F: Field + Group> Assignment<F> for MockProver<F> {
assert!(self.current_region.is_none());
self.current_region = Some(Region {
name: name().into(),
start: None,
columns: HashSet::default(),
rows: None,
enabled_selectors: HashMap::default(),
cells: vec![],
});
@ -449,7 +492,7 @@ impl<F: Field + Group> Assignment<F> for MockProver<F> {
}
if let Some(region) = self.current_region.as_mut() {
region.update_start(row);
region.update_extent(column.into(), row);
region.cells.push((column.into(), row));
}
@ -480,7 +523,7 @@ impl<F: Field + Group> Assignment<F> for MockProver<F> {
}
if let Some(region) = self.current_region.as_mut() {
region.update_start(row);
region.update_extent(column.into(), row);
region.cells.push((column.into(), row));
}
@ -656,7 +699,7 @@ impl<F: FieldExt> MockProver<F> {
gate: (gate_index, gate.name()).into(),
region: (r_i, r.name.clone()).into(),
column: cell.column,
offset: cell_row as isize - r.start.unwrap() as isize,
offset: cell_row as isize - r.rows.unwrap().0 as isize,
})
}
})
@ -815,9 +858,64 @@ impl<F: FieldExt> MockProver<F> {
if lookup_passes {
None
} else {
let input_columns: HashSet<Column<Any>> = lookup
.input_expressions
.iter()
.flat_map(|expression| {
expression.evaluate(
&|_| vec![],
&|_| {
panic!(
"virtual selectors are removed during optimization"
)
},
&|index, _, _| vec![self.cs.fixed_queries[index].0.into()],
&|index, _, _| vec![self.cs.advice_queries[index].0.into()],
&|index, _, _| {
vec![self.cs.instance_queries[index].0.into()]
},
&|a| a,
&|mut a, mut b| {
a.append(&mut b);
a
},
&|mut a, mut b| {
a.append(&mut b);
a
},
&|a, _| a,
)
})
.collect();
println!("Lookup input columns: {:?}", &input_columns);
// Figure out whether the lookup location
// overlaps an assigned region.
let location = self
.regions
.iter()
.enumerate()
.find(|(_, r)| {
let (start, end) = r.rows.unwrap();
// We match the region if any input columns overlap,
// rather than all of them, because matching complex
// selector columns is hard. This is probably going to
// cause some mismatches, which we'll fix later.
input_row >= start
&& input_row <= end
&& !input_columns.is_disjoint(&r.columns)
})
.map(|(r_i, r)| LookupFailure::InRegion {
region: (r_i, r.name.clone()).into(),
offset: input_row as usize - r.rows.unwrap().0 as usize,
})
.unwrap_or_else(|| LookupFailure::OutsideRegion {
row: input_row as usize,
});
Some(VerifyFailure::Lookup {
lookup_index,
row: input_row as usize,
location,
})
}
})
@ -891,10 +989,10 @@ impl<F: FieldExt> MockProver<F> {
mod tests {
use pasta_curves::Fp;
use super::{MockProver, VerifyFailure};
use super::{LookupFailure, MockProver, VerifyFailure};
use crate::{
circuit::{Layouter, SimpleFloorPlanner},
plonk::{Advice, Any, Circuit, Column, ConstraintSystem, Error, Selector},
plonk::{Advice, Any, Circuit, Column, ConstraintSystem, Error, Selector, TableColumn},
poly::Rotation,
};
@ -969,4 +1067,110 @@ mod tests {
}])
);
}
#[test]
fn bad_lookup() {
const K: u32 = 4;
#[derive(Clone)]
struct FaultyCircuitConfig {
a: Column<Advice>,
q: Selector,
table: TableColumn,
}
struct FaultyCircuit {}
impl Circuit<Fp> for FaultyCircuit {
type Config = FaultyCircuitConfig;
type FloorPlanner = SimpleFloorPlanner;
fn configure(meta: &mut ConstraintSystem<Fp>) -> Self::Config {
let a = meta.advice_column();
let q = meta.complex_selector();
let table = meta.lookup_table_column();
meta.lookup(|cells| {
let a = cells.query_advice(a, Rotation::cur());
let q = cells.query_selector(q);
// If q is enabled, a must be in the table.
// Zero is in the table, which satisfies the disabled case.
vec![(q * a, table)]
});
FaultyCircuitConfig { a, q, table }
}
fn without_witnesses(&self) -> Self {
Self {}
}
fn synthesize(
&self,
config: Self::Config,
mut layouter: impl Layouter<Fp>,
) -> Result<(), Error> {
layouter.assign_table(
|| "Doubling table",
|mut table| {
(0..(1 << (K - 1)))
.map(|i| {
table.assign_cell(
|| format!("table[{}] = {}", i, 2 * i),
config.table,
i,
|| Ok(Fp::from(2 * i as u64)),
)
})
.fold(Ok(()), |acc, res| acc.and(res))
},
)?;
layouter.assign_region(
|| "Good synthesis",
|mut region| {
// Enable the lookup on rows 0 and 1.
config.q.enable(&mut region, 0)?;
config.q.enable(&mut region, 1)?;
// Assign a = 2 and a = 6.
region.assign_advice(|| "a = 2", config.a, 0, || Ok(Fp::from(2)))?;
region.assign_advice(|| "a = 6", config.a, 1, || Ok(Fp::from(6)))?;
Ok(())
},
)?;
layouter.assign_region(
|| "Faulty synthesis",
|mut region| {
// Enable the lookup on rows 0 and 1.
config.q.enable(&mut region, 0)?;
config.q.enable(&mut region, 1)?;
// Assign a = 4.
region.assign_advice(|| "a = 4", config.a, 0, || Ok(Fp::from(4)))?;
// BUG: Assign a = 5, which doesn't exist in the table!
region.assign_advice(|| "a = 5", config.a, 1, || Ok(Fp::from(5)))?;
Ok(())
},
)
}
}
let prover = MockProver::run(K, &FaultyCircuit {}, vec![]).unwrap();
assert_eq!(
prover.verify(),
Err(vec![VerifyFailure::Lookup {
lookup_index: 0,
location: LookupFailure::InRegion {
region: (2, "Faulty synthesis".to_owned()).into(),
offset: 1,
}
}])
);
}
}