diff --git a/stdlib/src/blackbox_fallbacks/logic_fallbacks.rs b/stdlib/src/blackbox_fallbacks/logic_fallbacks.rs index 7ae0d59e9..24d5ff235 100644 --- a/stdlib/src/blackbox_fallbacks/logic_fallbacks.rs +++ b/stdlib/src/blackbox_fallbacks/logic_fallbacks.rs @@ -1,4 +1,6 @@ -use super::utils::bit_decomposition; +use crate::{blackbox_fallbacks::utils::mul_with_witness, helpers::VariableStore}; + +use super::utils::{bit_decomposition, boolean_expr}; use acir::{ acir_field::FieldElement, circuit::Opcode, @@ -6,18 +8,35 @@ use acir::{ }; // Range constraint -pub fn range(gate: Expression, bit_size: u32, num_witness: u32) -> (u32, Vec) { +pub fn range(gate: Expression, bit_size: u32, mut num_witness: u32) -> (u32, Vec) { + if bit_size == 1 { + let mut variables = VariableStore::new(&mut num_witness); + let bit_constraint = Opcode::Arithmetic(boolean_expr(&gate, &mut variables)); + return (variables.finalize(), vec![bit_constraint]); + } + let (new_gates, _, updated_witness_counter) = bit_decomposition(gate, bit_size, num_witness); (updated_witness_counter, new_gates) } +/// Returns a set of opcodes which constrain `a & b == result` +/// +/// `a` and `b` are assumed to be constrained to fit within `bit_size` externally. pub fn and( a: Expression, b: Expression, result: Witness, bit_size: u32, - num_witness: u32, + mut num_witness: u32, ) -> (u32, Vec) { + if bit_size == 1 { + let mut variables = VariableStore::new(&mut num_witness); + + let mut and_expr = mul_with_witness(&a, &b, &mut variables); + and_expr.push_addition_term(-FieldElement::one(), result); + + return (variables.finalize(), vec![Opcode::Arithmetic(and_expr)]); + } // Decompose the operands into bits // let (extra_gates_a, a_bits, updated_witness_counter) = @@ -53,13 +72,26 @@ pub fn and( (updated_witness_counter, new_gates) } +/// Returns a set of opcodes which constrain `a ^ b == result` +/// +/// `a` and `b` are assumed to be constrained to fit within `bit_size` externally. pub fn xor( a: Expression, b: Expression, result: Witness, bit_size: u32, - num_witness: u32, + mut num_witness: u32, ) -> (u32, Vec) { + if bit_size == 1 { + let mut variables = VariableStore::new(&mut num_witness); + + let product = mul_with_witness(&a, &b, &mut variables); + let mut xor_expr = &(&a + &b) - &product; + xor_expr.push_addition_term(-FieldElement::one(), result); + + return (variables.finalize(), vec![Opcode::Arithmetic(xor_expr)]); + } + // Decompose the operands into bits // let (extra_gates_a, a_bits, updated_witness_counter) = diff --git a/stdlib/src/blackbox_fallbacks/utils.rs b/stdlib/src/blackbox_fallbacks/utils.rs index 6b4c12ec2..853974afe 100644 --- a/stdlib/src/blackbox_fallbacks/utils.rs +++ b/stdlib/src/blackbox_fallbacks/utils.rs @@ -23,6 +23,59 @@ pub(crate) fn round_to_nearest_byte(num_bits: u32) -> u32 { round_to_nearest_mul_8(num_bits) / 8 } +pub(crate) fn boolean_expr(expr: &Expression, variables: &mut VariableStore) -> Expression { + &mul_with_witness(expr, expr, variables) - expr +} + +/// Returns an expression which represents `lhs * rhs` +/// +/// If one has multiplicative term and the other is of degree one or more, +/// the function creates [intermediate variables][`Witness`] accordingly. +/// There are two cases where we can optimize the multiplication between two expressions: +/// 1. If both expressions have at most a total degree of 1 in each term, then we can just multiply them +/// as each term in the result will be degree-2. +/// 2. If one expression is a constant, then we can just multiply the constant with the other expression +/// +/// (1) is because an [`Expression`] can hold at most a degree-2 univariate polynomial +/// which is what you get when you multiply two degree-1 univariate polynomials. +pub(crate) fn mul_with_witness( + lhs: &Expression, + rhs: &Expression, + variables: &mut VariableStore, +) -> Expression { + use std::borrow::Cow; + let lhs_is_linear = lhs.is_linear(); + let rhs_is_linear = rhs.is_linear(); + + // Case 1: Both expressions have at most a total degree of 1 in each term + if lhs_is_linear && rhs_is_linear { + return (lhs * rhs) + .expect("one of the expressions is a constant and so this should not fail"); + } + + // Case 2: One or both of the sides needs to be reduced to a degree-1 univariate polynomial + let lhs_reduced = if lhs_is_linear { + Cow::Borrowed(lhs) + } else { + Cow::Owned(variables.new_variable().into()) + }; + + // If the lhs and rhs are the same, then we do not need to reduce + // rhs, we only need to square the lhs. + if lhs == rhs { + return (&*lhs_reduced * &*lhs_reduced) + .expect("Both expressions are reduced to be degree<=1"); + }; + + let rhs_reduced = if rhs_is_linear { + Cow::Borrowed(rhs) + } else { + Cow::Owned(variables.new_variable().into()) + }; + + (&*lhs_reduced * &*rhs_reduced).expect("Both expressions are reduced to be degree<=1") +} + // Generates opcodes and directives to bit decompose the input `gate` // Returns the bits and the updated witness counter // TODO:Ideally, we return the updated witness counter, or we require the input @@ -57,9 +110,7 @@ pub(crate) fn bit_decomposition( let two = FieldElement::from(2_i128); for &bit in &bit_vector { // Bit constraint to ensure each bit is a zero or one; bit^2 - bit = 0 - let mut expr = Expression::default(); - expr.push_multiplication_term(FieldElement::one(), bit, bit); - expr.push_addition_term(-FieldElement::one(), bit); + let expr = boolean_expr(&bit.into(), &mut variables); binary_exprs.push(Opcode::Arithmetic(expr)); // Constraint to ensure that the bits are constrained to be a bit decomposition