Skip to content
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

Binary solver for array equality constraints #520

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1424f86
add a binary solver for array equality constraints
guipublic Nov 24, 2022
cd78f4c
Merge branch 'master' into gd/auto_return_array
guipublic Nov 24, 2022
2320abd
Merge branch 'master' into gd/auto_return_array
guipublic Nov 24, 2022
b515216
Merge branch 'master' into gd/auto_return_array
guipublic Nov 29, 2022
94ae8ee
Merge branch 'master' into gd/auto_return_array
guipublic Nov 30, 2022
673d03f
Code review
guipublic Dec 1, 2022
719d8d2
Code review
guipublic Dec 2, 2022
146d1e9
Merge branch 'master' into gd/auto_return_array
guipublic Dec 2, 2022
e4166c0
Code review
guipublic Dec 2, 2022
32b7c21
Merge branch 'master' into gd/auto_return_array
guipublic Dec 2, 2022
d84d0f8
Merge branch 'master' into gd/auto_return_array
guipublic Dec 6, 2022
ff19a06
Merge branch 'master' into gd/auto_return_array
guipublic Dec 7, 2022
eb2555e
code review: remove the closure
guipublic Dec 7, 2022
3d430db
Code review
guipublic Dec 9, 2022
ffe2674
Merge branch 'master' into gd/auto_return_array
guipublic Dec 9, 2022
52b90c9
Merge branch 'master' into gd/auto_return_array
guipublic Dec 12, 2022
aa7093d
Code review: remove pass number
guipublic Dec 12, 2022
e0e03ab
fix the build
guipublic Dec 12, 2022
0574d0f
Merge branch 'master' into gd/auto_return_array
guipublic Dec 12, 2022
4d2e097
Merge branch 'master' into gd/auto_return_array
guipublic Dec 12, 2022
da10422
Code review
guipublic Dec 13, 2022
33b922d
Merge branch 'master' into gd/auto_return_array
guipublic Dec 13, 2022
395d1b0
Merge branch 'master' into gd/auto_return_array
guipublic Dec 13, 2022
75dee9d
Merge branch 'master' into gd/auto_return_array
guipublic Jan 12, 2023
3038995
Merge branch 'master' into gd/auto_return_array
guipublic Jan 13, 2023
26507f9
Merge branch 'master' into gd/auto_return_array
guipublic Jan 16, 2023
5062107
Merge branch 'master' into gd/auto_return_array
guipublic Jan 17, 2023
7114370
Merge branch 'master' into gd/auto_return_array
guipublic Jan 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
323 changes: 176 additions & 147 deletions crates/acvm/src/lib.rs

Large diffs are not rendered by default.

93 changes: 68 additions & 25 deletions crates/acvm/src/pwg/arithmetic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,23 @@ impl ArithmeticSolver {
}
}

fn solve_mul_term_helper(
term: &(FieldElement, Witness, Witness),
witness_assignments: &BTreeMap<Witness, FieldElement>,
) -> MulTerm {
let (q_m, w_l, w_r) = term;
// Check if these values are in the witness assignments
let w_l_value = witness_assignments.get(w_l);
let w_r_value = witness_assignments.get(w_r);

match (w_l_value, w_r_value) {
(None, None) => MulTerm::TooManyUnknowns,
(Some(w_l), Some(w_r)) => MulTerm::Solved(*q_m * *w_l * *w_r),
(None, Some(w_r)) => MulTerm::OneUnknown(*q_m * *w_r, *w_l),
(Some(w_l), None) => MulTerm::OneUnknown(*q_m * *w_l, *w_r),
}
}

/// Returns the evaluation of the multiplication term in the arithmetic gate
/// If the witness values are not known, then the function returns a None
/// XXX: Do we need to account for the case where 5xy + 6x = 0 ? We do not know y, but it can be solved given x . But I believe x can be solved with another gate
Expand All @@ -118,26 +135,24 @@ impl ArithmeticSolver {
// We are assuming it has been optimised.
match arith_gate.mul_terms.len() {
0 => MulTerm::Solved(FieldElement::zero()),
1 => {
let q_m = &arith_gate.mul_terms[0].0;
let w_l = &arith_gate.mul_terms[0].1;
let w_r = &arith_gate.mul_terms[0].2;

// Check if these values are in the witness assignments
let w_l_value = witness_assignments.get(w_l);
let w_r_value = witness_assignments.get(w_r);

match (w_l_value, w_r_value) {
(None, None) => MulTerm::TooManyUnknowns,
(Some(w_l), Some(w_r)) => MulTerm::Solved(*q_m * *w_l * *w_r),
(None, Some(w_r)) => MulTerm::OneUnknown(*q_m * *w_r, *w_l),
(Some(w_l), None) => MulTerm::OneUnknown(*q_m * *w_l, *w_r),
}
}
1 => ArithmeticSolver::solve_mul_term_helper(
&arith_gate.mul_terms[0],
witness_assignments,
),
_ => panic!("Mul term in the arithmetic gate must contain either zero or one term"),
}
}

fn solve_fan_in_term_helper(
term: &(FieldElement, Witness),
witness_assignments: &BTreeMap<Witness, FieldElement>,
) -> Option<FieldElement> {
let (q_l, w_l) = term;
// Check if we have w_l
let w_l_value = witness_assignments.get(w_l);
w_l_value.map(|a| *q_l * *a)
}

/// Returns the summation of all of the variables, plus the unknown variable
/// Returns None, if there is more than one unknown variable
/// We cannot assign
Expand All @@ -154,19 +169,14 @@ impl ArithmeticSolver {
let mut result = FieldElement::zero();

for term in arith_gate.linear_combinations.iter() {
let q_l = term.0;
let w_l = &term.1;

// Check if we have w_l
let w_l_value = witness_assignments.get(w_l);

match w_l_value {
Some(a) => result += q_l * *a,
let value = ArithmeticSolver::solve_fan_in_term_helper(term, witness_assignments);
match value {
Some(a) => result += a,
None => {
unknown_variable = *term;
num_unknowns += 1;
}
};
}

// If we have more than 1 unknown, then we cannot solve this equation
if num_unknowns > 1 {
Expand All @@ -180,6 +190,39 @@ impl ArithmeticSolver {

GateStatus::GateSolvable(result, unknown_variable)
}

// Partially evaluate the gate using the known witnesses
pub fn evaluate(
expr: &Expression,
initial_witness: &BTreeMap<Witness, FieldElement>,
) -> Expression {
let mut result = Expression::default();
for &(c, w1, w2) in &expr.mul_terms {
let mul_result = ArithmeticSolver::solve_mul_term_helper(&(c, w1, w2), initial_witness);
match mul_result {
MulTerm::OneUnknown(v, w) => {
if !v.is_zero() {
result.linear_combinations.push((v, w));
}
}
MulTerm::TooManyUnknowns => {
if !c.is_zero() {
result.mul_terms.push((c, w1, w2));
}
}
MulTerm::Solved(f) => result.q_c += f,
}
}
for &(c, w) in &expr.linear_combinations {
if let Some(f) = ArithmeticSolver::solve_fan_in_term_helper(&(c, w), initial_witness) {
result.q_c += f;
} else if !c.is_zero() {
result.linear_combinations.push((c, w));
}
}
result.q_c += expr.q_c;
result
}
}

#[test]
Expand Down
234 changes: 234 additions & 0 deletions crates/acvm/src/pwg/binary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
use std::collections::{BTreeMap, HashMap, HashSet};

use acir::{
circuit::{gate::Directive, Gate},
native_types::{Expression, Witness},
FieldElement,
};
use num_bigint::BigUint;
use num_traits::{One, Zero};

use crate::GateResolution;

pub struct BinarySolver {
binary_witness: HashSet<Witness>,
invert_witness: HashMap<Witness, Witness>,
positive_witness: HashMap<Witness, BigUint>,
}

impl Default for BinarySolver {
fn default() -> Self {
Self::new()
}
}

kevaundray marked this conversation as resolved.
Show resolved Hide resolved
impl BinarySolver {
pub fn new() -> BinarySolver {
BinarySolver {
binary_witness: HashSet::new(),
invert_witness: HashMap::new(),
positive_witness: HashMap::new(),
}
}

pub fn is_boolean(&self, w: &Witness) -> bool {
self.binary_witness.contains(w)
}

pub fn are_inverse(&self, w1: &Witness, w2: &Witness) -> bool {
self.invert_witness.get(w1) == Some(w2) || self.invert_witness.get(w2) == Some(w1)
}

pub fn are_boolean(&self, w1: &Witness, w2: &Witness) -> bool {
self.are_inverse(w1, w2) || (self.is_boolean(w1) && self.is_boolean(w2))
}

pub fn get_max_value(&self, w: &Witness) -> Option<BigUint> {
if self.is_boolean(w) {
Some(BigUint::one())
} else {
self.positive_witness.get(w).cloned()
}
}

pub fn solve(
&mut self,
gate: &Gate,
initial_witness: &mut BTreeMap<Witness, FieldElement>,
) -> GateResolution {
let mut result = GateResolution::Skip;
if let Gate::Arithmetic(arith) = gate {
let partial_gate =
super::arithmetic::ArithmeticSolver::evaluate(arith, initial_witness);
result = self.solve_booleans(initial_witness, &partial_gate);
self.identify_booleans(&partial_gate);
} else {
self.lookup_binaries(gate);
}
result
}

/// Solve (some) arithemtic expression which is only using booleans
pub fn solve_booleans(
&self,
initial_witness: &mut BTreeMap<Witness, FieldElement>,
gate: &Expression,
) -> GateResolution {
let result = self.solve_inverse(gate, initial_witness);
match result {
GateResolution::Resolved
| GateResolution::UnsatisfiedConstrain
| GateResolution::UnknownError(_) => return result,
GateResolution::Skip => (),
GateResolution::UnsupportedOpcode(_) | GateResolution::Solved(_) => unreachable!(),
}

if let Some(max) = self.is_binary(gate) {
if max < FieldElement::modulus() {
if gate.q_c == FieldElement::zero() {
for (_, w) in &gate.linear_combinations {
initial_witness.insert(*w, FieldElement::zero());
}
GateResolution::Resolved
} else {
GateResolution::UnsatisfiedConstrain
}
} else {
GateResolution::Skip
}
} else {
GateResolution::Skip
}
}

// checks whether the expression uses only booleans/positive witness and returns the max value of the expression in that case
fn is_binary(&self, gate: &Expression) -> Option<BigUint> {
let mut max = BigUint::zero();
for (c, w1, w2) in &gate.mul_terms {
if !self.are_boolean(w1, w2) {
return None;
}
max += BigUint::from_bytes_be(&c.to_bytes());
}
for (c, w) in &gate.linear_combinations {
if let Some(v) = self.get_max_value(w) {
max += BigUint::from_bytes_be(&c.to_bytes()) * v;
} else {
return None;
}
}
if max > FieldElement::modulus() {
return None;
}
Some(max + BigUint::from_bytes_be(&gate.q_c.to_bytes()))
}

fn solve_inverse(
&self,
gate: &Expression,
initial_witness: &mut BTreeMap<Witness, FieldElement>,
) -> GateResolution {
if gate.mul_terms.len() == 1
&& self.are_inverse(&gate.mul_terms[0].1, &gate.mul_terms[0].2)
&& gate.linear_combinations.is_empty()
{
if gate.q_c.is_zero() {
initial_witness.insert(gate.mul_terms[0].1, FieldElement::zero());
initial_witness.insert(gate.mul_terms[0].2, FieldElement::zero());
return GateResolution::Resolved;
} else if !gate.q_c.is_one() {
return GateResolution::UnsatisfiedConstrain;
}
}

GateResolution::Skip
}

// look for boolean constraint and add boolean witness to a map
pub fn identify_booleans(&mut self, arith: &Expression) {
let mut x = None;
if arith.mul_terms.len() == 1 && arith.linear_combinations.len() == 1 {
// x*x = x
if arith.mul_terms[0].1 == arith.mul_terms[0].2
&& arith.mul_terms[0].1 == arith.linear_combinations[0].1
&& arith.q_c.is_zero()
&& (arith.mul_terms[0].0 + arith.linear_combinations[0].0).is_zero()
{
x = Some(0);
} else {
// x = a*b, a,b booleans or inverse
if self.are_boolean(&arith.mul_terms[0].1, &arith.mul_terms[0].2) {
if arith.q_c.is_zero() {
if (arith.mul_terms[0].0 + arith.linear_combinations[0].0).is_zero() {
x = Some(0);
}
} else if (arith.mul_terms[0].0 + arith.q_c).is_zero()
&& arith.mul_terms[0].0 == arith.linear_combinations[0].0
{
x = Some(0);
}
}
}
} else if arith.mul_terms.is_empty() && arith.linear_combinations.len() == 2 {
//x=y
let z = if self.is_boolean(&arith.linear_combinations[0].1)
&& !self.is_boolean(&arith.linear_combinations[1].1)
{
Some(1)
} else if self.is_boolean(&arith.linear_combinations[1].1)
&& !self.is_boolean(&arith.linear_combinations[0].1)
{
Some(0)
} else {
None
};
if z.is_some() {
if arith.q_c.is_zero() {
if (arith.linear_combinations[0].0 + arith.linear_combinations[1].0).is_zero() {
x = z;
}
} else if (arith.q_c + arith.linear_combinations[1].0).is_zero()
&& arith.linear_combinations[0].0 == arith.linear_combinations[1].0
{
x = Some(0);
}
}
} else if arith.mul_terms.is_empty() && arith.linear_combinations.len() > 2 {
//"binary" gates 'optimised' by the optimiser should have an intermediate variable and a bunch of booleans
let mut max = BigUint::from_bytes_be(&arith.q_c.to_bytes());
for (i, lin) in arith.linear_combinations.iter().enumerate() {
if let Some(v) = self.get_max_value(&lin.1) {
max += v * BigUint::from_bytes_be(&lin.0.to_bytes());
} else if x.is_some() {
x = None;
break;
} else {
x = Some(i);
}
}
if max < FieldElement::modulus()
&& x.is_some()
&& arith.linear_combinations[x.unwrap()].0 == -FieldElement::one()
{
self.positive_witness.insert(arith.linear_combinations[x.unwrap()].1, max);
x = None;
}
}
if let Some(x) = x {
self.binary_witness.insert(arith.linear_combinations[x].1);
}
}

// look for boolean and inverse constraints
pub fn lookup_binaries(&mut self, gate: &Gate) {
jfecher marked this conversation as resolved.
Show resolved Hide resolved
match gate {
Gate::Directive(Directive::Invert { x, result }) => {
self.invert_witness.insert(*x, *result);
}
Gate::Arithmetic(a) => {
self.identify_booleans(a);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is this branch triggered?

seems identify_binaries is called when it is not an arithmetic gate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, but still the function is correct like this, using unreachable! would make it fit for the current use and less versatile.

}
_ => (),
}
}
}
Loading