Skip to content

Commit

Permalink
chore!: Rename Arithmetic opcode to AssertZero (#3840)
Browse files Browse the repository at this point in the history
# Description

When this opcode is given to a backend, they assert that the expression
inside of the opcode is zero, so this changes the naming to more align
with that.

This is breaking as it does change the serialization that backends use  

## Problem\*

Resolves <!-- Link to GitHub Issue -->

## Summary\*



## Additional Context



## Documentation\*

Check one:
- [ ] No documentation needed.
- [ ] Documentation included in this PR.
- [ ] **[Exceptional Case]** Documentation to be submitted in a separate
PR.

# PR Checklist\*

- [ ] I have tested the changes locally.
- [ ] I have formatted the changes with [Prettier](https://prettier.io/)
and/or `cargo fmt` on default settings.

---------

Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com>
  • Loading branch information
kevaundray and TomAFrench authored Dec 19, 2023
1 parent 8f5cd6c commit 836f171
Show file tree
Hide file tree
Showing 19 changed files with 92 additions and 86 deletions.
14 changes: 7 additions & 7 deletions acvm-repo/acir/acir_docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,22 @@ Some opcodes are not constrained, which mean they will not be used by the provin

Finally, some opcodes will have a predicate, whose value is 0 or 1. Its purpose is to nullify the opcode when the value is 0, so that it has no effect. Note that removing the opcode is not a solution because this modifies the circuit (the circuit being mainly the list of the opcodes).

*Remark*: Opcodes operate on witnesses, but we will see that some opcode work on Arithmetic expressions of witnesses. We call an arithmetic expression a linear combination of witnesses and/or products of two witnesses (and also a constant term). A single witness is a (simple) arithmetic expression, and conversly, an arithmetic expression can be turned into a single witness using an arithmetic opcode (see below). So basically, using witnesses or arithmetic expressions is equivalent, but the latter can avoid the creation of witness in some cases.
*Remark*: Opcodes operate on witnesses, but we will see that some opcode work on expressions of witnesses. We call an expression a linear combination of witnesses and/or products of two witnesses (and also a constant term). A single witness is a (simple) expression, and conversely, an expression can be turned into a single witness using an assert-zero opcode (see below). So basically, using witnesses or expressions is equivalent, but the latter can avoid the creation of witness in some cases.

### Arithmetic opcode
An arithmetic opcode adds the constraint that P(w) = 0, where w=(w_1,..w_n) is a tuple of n witnesses, and P is a multi-variate polynomial of total degree at most 2.
### AssertZero opcode
An AssertZero opcode adds the constraint that P(w) = 0, where w=(w_1,..w_n) is a tuple of n witnesses, and P is a multi-variate polynomial of total degree at most 2.
The coefficients ${q_M}_{i,j}, q_i,q_c$ of the polynomial are known values which define the opcode.
A general expression of arithmetic opcode is the following: $\sum_{i,j} {q_M}_{i,j}w_iw_j + \sum_i q_iw_i +q_c = 0$
A general expression of assert-zero opcode is the following: $\sum_{i,j} {q_M}_{i,j}w_iw_j + \sum_i q_iw_i +q_c = 0$

An arithmetic opcode can be used to:
An assert-zero opcode can be used to:
- **express a constraint** on witnesses; for instance to express that a witness $w$ is a boolean, you can add the opcode: $w*w-w=0$
- or, to **compute the value** of an arithmetic operation of some inputs. For instance, to multiply two witnesses $x$ and $y$, you would use the opcode $z-x*y=0$, which would constraint $z$ to be $x*y$.


The solver expects that at most one witness is not known when executing the opcode.

### BlackBoxFuncCall opcode
These opcodes represent a specific computation. Even if any computation can be done using only arithmetic opcodes, it is not always efficient. Some proving systems, and in particular the proving system from Aztec, can implement several computations more efficiently using for instance look-up tables. The BlackBoxFuncCall opcode is used to ask the proving system to handle the computation by itself.
These opcodes represent a specific computation. Even if any computation can be done using only assert-zero opcodes, it is not always efficient. Some proving systems, and in particular the proving system from Aztec, can implement several computations more efficiently using for instance look-up tables. The BlackBoxFuncCall opcode is used to ask the proving system to handle the computation by itself.
All black box functions takes as input a tuple (witness, num_bits), where num_bits is a constant representing the bit size of the input witness, and they have one or several witnesses as output.
Some more advanced computations assume that the proving system has an 'embedded curve'. It is a curve that cycle with the main curve of the proving system, i.e the scalar field of the embedded curve is the base field of the main one, and vice-versa. The curves used by the proving system are dependent on the proving system (and/or its configuration). Aztec's Barretenberg uses BN254 as the main curve and Grumpkin as the embedded curve.

Expand Down Expand Up @@ -180,7 +180,7 @@ This opcode is used as a hint for the solver when executing (solving) the circui
- predicate: an arithmetic expression that disable the opcode when it is null.

Let's see an example with euclidian division.
The normal way to compute a/b, where a and b are 8-bits integers, is to implement Euclid algorithm which computes in a loop (or recursively) modulos of the kind 'a mod b'. Doing this computation requires a lot of steps to be properly implemented in ACIR, especially the loop with a condition. However, euclidian division can be easily constrained with one arithmetic opcode: a = bq+r, assuming q is 8 bits and r<b. Since these assumptions can easily written with a few range opcodes, euclidian division can in fact be implemented with a small number of opcodes.
The normal way to compute a/b, where a and b are 8-bits integers, is to implement Euclid algorithm which computes in a loop (or recursively) modulos of the kind 'a mod b'. Doing this computation requires a lot of steps to be properly implemented in ACIR, especially the loop with a condition. However, euclidian division can be easily constrained with one assert-zero opcode: a = bq+r, assuming q is 8 bits and r<b. Since these assumptions can easily written with a few range opcodes, euclidian division can in fact be implemented with a small number of opcodes.

However, in order to write these opcodes we need the result of the division which are the witness q and r. But from the constraint a=bq+r, how can the solver figure out how to solve q and r with only one equation? This is where brillig/unconstrained function come into action. We simply define a function that performs the usual Euclid algorithm to compute q and r from a and b. Since Brillig opcode does not generate constraint, it won't be provided to the proving system but simply used by the solver to compute the values of q and r.

Expand Down
24 changes: 12 additions & 12 deletions acvm-repo/acir/codegen/acir.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -767,12 +767,12 @@ namespace Circuit {

struct Opcode {

struct Arithmetic {
struct AssertZero {
Circuit::Expression value;

friend bool operator==(const Arithmetic&, const Arithmetic&);
friend bool operator==(const AssertZero&, const AssertZero&);
std::vector<uint8_t> bincodeSerialize() const;
static Arithmetic bincodeDeserialize(std::vector<uint8_t>);
static AssertZero bincodeDeserialize(std::vector<uint8_t>);
};

struct BlackBoxFuncCall {
Expand Down Expand Up @@ -818,7 +818,7 @@ namespace Circuit {
static MemoryInit bincodeDeserialize(std::vector<uint8_t>);
};

std::variant<Arithmetic, BlackBoxFuncCall, Directive, Brillig, MemoryOp, MemoryInit> value;
std::variant<AssertZero, BlackBoxFuncCall, Directive, Brillig, MemoryOp, MemoryInit> value;

friend bool operator==(const Opcode&, const Opcode&);
std::vector<uint8_t> bincodeSerialize() const;
Expand Down Expand Up @@ -4268,20 +4268,20 @@ Circuit::Opcode serde::Deserializable<Circuit::Opcode>::deserialize(Deserializer

namespace Circuit {

inline bool operator==(const Opcode::Arithmetic &lhs, const Opcode::Arithmetic &rhs) {
inline bool operator==(const Opcode::AssertZero &lhs, const Opcode::AssertZero &rhs) {
if (!(lhs.value == rhs.value)) { return false; }
return true;
}

inline std::vector<uint8_t> Opcode::Arithmetic::bincodeSerialize() const {
inline std::vector<uint8_t> Opcode::AssertZero::bincodeSerialize() const {
auto serializer = serde::BincodeSerializer();
serde::Serializable<Opcode::Arithmetic>::serialize(*this, serializer);
serde::Serializable<Opcode::AssertZero>::serialize(*this, serializer);
return std::move(serializer).bytes();
}

inline Opcode::Arithmetic Opcode::Arithmetic::bincodeDeserialize(std::vector<uint8_t> input) {
inline Opcode::AssertZero Opcode::AssertZero::bincodeDeserialize(std::vector<uint8_t> input) {
auto deserializer = serde::BincodeDeserializer(input);
auto value = serde::Deserializable<Opcode::Arithmetic>::deserialize(deserializer);
auto value = serde::Deserializable<Opcode::AssertZero>::deserialize(deserializer);
if (deserializer.get_buffer_offset() < input.size()) {
throw serde::deserialization_error("Some input bytes were not read");
}
Expand All @@ -4292,14 +4292,14 @@ namespace Circuit {

template <>
template <typename Serializer>
void serde::Serializable<Circuit::Opcode::Arithmetic>::serialize(const Circuit::Opcode::Arithmetic &obj, Serializer &serializer) {
void serde::Serializable<Circuit::Opcode::AssertZero>::serialize(const Circuit::Opcode::AssertZero &obj, Serializer &serializer) {
serde::Serializable<decltype(obj.value)>::serialize(obj.value, serializer);
}

template <>
template <typename Deserializer>
Circuit::Opcode::Arithmetic serde::Deserializable<Circuit::Opcode::Arithmetic>::deserialize(Deserializer &deserializer) {
Circuit::Opcode::Arithmetic obj;
Circuit::Opcode::AssertZero serde::Deserializable<Circuit::Opcode::AssertZero>::deserialize(Deserializer &deserializer) {
Circuit::Opcode::AssertZero obj;
obj.value = serde::Deserializable<decltype(obj.value)>::deserialize(deserializer);
return obj;
}
Expand Down
2 changes: 1 addition & 1 deletion acvm-repo/acir/src/circuit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ mod tests {
let circuit = Circuit {
current_witness_index: 0,
opcodes: vec![
Opcode::Arithmetic(crate::native_types::Expression {
Opcode::AssertZero(crate::native_types::Expression {
mul_terms: vec![],
linear_combinations: vec![],
q_c: FieldElement::from(8u128),
Expand Down
4 changes: 2 additions & 2 deletions acvm-repo/acir/src/circuit/opcodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub use memory_operation::{BlockId, MemOp};

#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Opcode {
Arithmetic(Expression),
AssertZero(Expression),
/// Calls to "gadgets" which rely on backends implementing support for specialized constraints.
///
/// Often used for exposing more efficient implementations of SNARK-unfriendly computations.
Expand All @@ -36,7 +36,7 @@ pub enum Opcode {
impl std::fmt::Display for Opcode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Opcode::Arithmetic(expr) => {
Opcode::AssertZero(expr) => {
write!(f, "EXPR [ ")?;
for i in &expr.mul_terms {
write!(f, "({}, _{}, _{}) ", i.0, i.1.witness_index(), i.2.witness_index())?;
Expand Down
14 changes: 10 additions & 4 deletions acvm-repo/acir/src/native_types/expression/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ mod ordering;

// In the addition polynomial
// We can have arbitrary fan-in/out, so we need more than wL,wR and wO
// When looking at the arithmetic opcode for the quotient polynomial in standard plonk
// When looking at the assert-zero opcode for the quotient polynomial in standard plonk
// You can think of it as fan-in 2 and fan out-1 , or you can think of it as fan-in 1 and fan-out 2
//
// In the multiplication polynomial
// XXX: If we allow the degree of the quotient polynomial to be arbitrary, then we will need a vector of wire values
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub struct Expression {
// To avoid having to create intermediate variables pre-optimization
// We collect all of the multiplication terms in the arithmetic opcode
// We collect all of the multiplication terms in the assert-zero opcode
// A multiplication term if of the form q_M * wL * wR
// Hence this vector represents the following sum: q_M1 * wL1 * wR1 + q_M2 * wL2 * wR2 + .. +
pub mul_terms: Vec<(FieldElement, Witness, Witness)>,
Expand All @@ -42,7 +42,7 @@ impl std::fmt::Display for Expression {
if let Some(witness) = self.to_witness() {
write!(f, "x{}", witness.witness_index())
} else {
write!(f, "%{:?}%", crate::circuit::opcodes::Opcode::Arithmetic(self.clone()))
write!(f, "%{:?}%", crate::circuit::opcodes::Opcode::AssertZero(self.clone()))
}
}
}
Expand Down Expand Up @@ -178,7 +178,13 @@ impl Expression {
self.linear_combinations.sort_by(|a, b| a.1.cmp(&b.1));
}

/// Checks if this polynomial can fit into one arithmetic identity
/// Checks if this expression can fit into one arithmetic identity
/// TODO: This needs to be reworded, arithmetic identity only makes sense in the context
/// TODO of PLONK, whereas we want expressions to be generic.
/// TODO: We just need to reword it to say exactly what its doing and
/// TODO then reference the fact that this is what plonk will accept.
/// TODO alternatively, we can define arithmetic identity in the context of expressions
/// TODO and then reference that.
pub fn fits_in_one_identity(&self, width: usize) -> bool {
// A Polynomial with more than one mul term cannot fit into one opcode
if self.mul_terms.len() > 1 {
Expand Down
2 changes: 1 addition & 1 deletion acvm-repo/acir/tests/test_program_serialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use brillig::{HeapArray, RegisterIndex, RegisterOrMemory};

#[test]
fn addition_circuit() {
let addition = Opcode::Arithmetic(Expression {
let addition = Opcode::AssertZero(Expression {
mul_terms: Vec::new(),
linear_combinations: vec![
(FieldElement::one(), Witness(1)),
Expand Down
4 changes: 2 additions & 2 deletions acvm-repo/acvm/src/compiler/optimizers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ pub(super) fn optimize_internal(acir: Circuit) -> (Circuit, Vec<usize>) {
.opcodes
.into_iter()
.map(|opcode| {
if let Opcode::Arithmetic(arith_expr) = opcode {
Opcode::Arithmetic(GeneralOptimizer::optimize(arith_expr))
if let Opcode::AssertZero(arith_expr) = opcode {
Opcode::AssertZero(GeneralOptimizer::optimize(arith_expr))
} else {
opcode
}
Expand Down
12 changes: 6 additions & 6 deletions acvm-repo/acvm/src/compiler/optimizers/redundant_range.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ fn extract_range_opcode(opcode: &Opcode) -> Option<(Witness, u32)> {

fn optimized_range_opcode(witness: Witness, num_bits: u32) -> Opcode {
if num_bits == 1 {
Opcode::Arithmetic(Expression {
Opcode::AssertZero(Expression {
mul_terms: vec![(FieldElement::one(), witness, witness)],
linear_combinations: vec![(-FieldElement::one(), witness)],
q_c: FieldElement::zero(),
Expand Down Expand Up @@ -234,13 +234,13 @@ mod tests {
#[test]
fn non_range_opcodes() {
// The optimizer should not remove or change non-range opcodes
// The four Arithmetic opcodes should remain unchanged.
// The four AssertZero opcodes should remain unchanged.
let mut circuit = test_circuit(vec![(Witness(1), 16), (Witness(1), 16)]);

circuit.opcodes.push(Opcode::Arithmetic(Expression::default()));
circuit.opcodes.push(Opcode::Arithmetic(Expression::default()));
circuit.opcodes.push(Opcode::Arithmetic(Expression::default()));
circuit.opcodes.push(Opcode::Arithmetic(Expression::default()));
circuit.opcodes.push(Opcode::AssertZero(Expression::default()));
circuit.opcodes.push(Opcode::AssertZero(Expression::default()));
circuit.opcodes.push(Opcode::AssertZero(Expression::default()));
circuit.opcodes.push(Opcode::AssertZero(Expression::default()));
let acir_opcode_positions = circuit.opcodes.iter().enumerate().map(|(i, _)| i).collect();
let optimizer = RangeOptimizer::new(circuit);
let (optimized_circuit, _) = optimizer.replace_redundant_ranges(acir_opcode_positions);
Expand Down
14 changes: 7 additions & 7 deletions acvm-repo/acvm/src/compiler/transformers/csat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ impl CSatTransformer {
}

// Still missing dead witness optimization.
// To do this, we will need the whole set of arithmetic opcodes
// To do this, we will need the whole set of assert-zero opcodes
// I think it can also be done before the local optimization seen here, as dead variables will come from the user
pub(crate) fn transform(
&mut self,
Expand All @@ -84,7 +84,7 @@ impl CSatTransformer {
opcode
}

// This optimization will search for combinations of terms which can be represented in a single arithmetic opcode
// This optimization will search for combinations of terms which can be represented in a single assert-zero opcode
// Case 1 : qM * wL * wR + qL * wL + qR * wR + qO * wO + qC
// This polynomial does not require any further optimizations, it can be safely represented in one opcode
// ie a polynomial with 1 mul(bi-variate) term and 3 (univariate) terms where 2 of those terms match the bivariate term
Expand All @@ -93,13 +93,13 @@ impl CSatTransformer {
//
//
// Case 2: qM * wL * wR + qL * wL + qR * wR + qO * wO + qC + qM2 * wL2 * wR2 + qL * wL2 + qR * wR2 + qO * wO2 + qC2
// This polynomial cannot be represented using one arithmetic opcode.
// This polynomial cannot be represented using one assert-zero opcode.
//
// This algorithm will first extract the first full opcode(if possible):
// t = qM * wL * wR + qL * wL + qR * wR + qO * wO + qC
//
// The polynomial now looks like so t + qM2 * wL2 * wR2 + qL * wL2 + qR * wR2 + qO * wO2 + qC2
// This polynomial cannot be represented using one arithmetic opcode.
// This polynomial cannot be represented using one assert-zero opcode.
//
// This algorithm will then extract the second full opcode(if possible):
// t2 = qM2 * wL2 * wR2 + qL * wL2 + qR * wR2 + qO * wO2 + qC2
Expand All @@ -121,7 +121,7 @@ impl CSatTransformer {
// If the opcode only has one mul term, then this algorithm cannot optimize it any further
// Either it can be represented in a single arithmetic equation or it's fan-in is too large and we need intermediate variables for those
// large-fan-in optimization is not this algorithms purpose.
// If the opcode has 0 mul terms, then it is an add opcode and similarly it can either fit into a single arithmetic opcode or it has a large fan-in
// If the opcode has 0 mul terms, then it is an add opcode and similarly it can either fit into a single assert-zero opcode or it has a large fan-in
if opcode.mul_terms.len() <= 1 {
return opcode;
}
Expand Down Expand Up @@ -194,7 +194,7 @@ impl CSatTransformer {
}
}

// Now we have used up 2 spaces in our arithmetic opcode. The width now dictates, how many more we can add
// Now we have used up 2 spaces in our assert-zero opcode. The width now dictates, how many more we can add
let mut remaining_space = self.width - 2 - 1; // We minus 1 because we need an extra space to contain the intermediate variable
// Keep adding terms until we have no more left, or we reach the width
let mut remaining_linear_terms =
Expand Down Expand Up @@ -325,7 +325,7 @@ impl CSatTransformer {
// Then use intermediate variables again to squash the fan-in, so that it can fit into the appropriate width

// First check if this polynomial actually needs a partial opcode optimization
// There is the chance that it fits perfectly within the arithmetic opcode
// There is the chance that it fits perfectly within the assert-zero opcode
if opcode.fits_in_one_identity(self.width) {
return opcode;
}
Expand Down
Loading

0 comments on commit 836f171

Please sign in to comment.