Skip to content
This repository has been archived by the owner on Apr 9, 2024. It is now read-only.

Commit

Permalink
feat: add optimisations to fallback black box functions on booleans (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
TomAFrench authored Jul 25, 2023
1 parent ceaf5fe commit 2cfb2a8
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 7 deletions.
40 changes: 36 additions & 4 deletions stdlib/src/blackbox_fallbacks/logic_fallbacks.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
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,
native_types::{Expression, Witness},
};

// Range constraint
pub fn range(gate: Expression, bit_size: u32, num_witness: u32) -> (u32, Vec<Opcode>) {
pub fn range(gate: Expression, bit_size: u32, mut num_witness: u32) -> (u32, Vec<Opcode>) {
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<Opcode>) {
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) =
Expand Down Expand Up @@ -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<Opcode>) {
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) =
Expand Down
57 changes: 54 additions & 3 deletions stdlib/src/blackbox_fallbacks/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 2cfb2a8

Please sign in to comment.