Skip to content

Commit

Permalink
feat: add encode to compiler
Browse files Browse the repository at this point in the history
This commit introduces `Compiler::encode` and `Compiler::decode`, two
functions that will create a compressed representation of a circuit that
can be used to generate prover and verifier keys.

The compression strategy takes advantage of the fact that circuit
representations are sparse; meaning, most of the scalars are zeroes. We
also have a higher incidence of `1` and `-1`, so any value that is
equivalent to a `i8` is compressed into a single byte, instead of the
regular `32` bytes of a bls12-381.

This will result in expressive gains in terms of storage. For instance,
a `2^12` circuit can be compressed into roughly 100Kb.
  • Loading branch information
vlopes11 committed Apr 2, 2023
1 parent 0487cc4 commit 7f9d305
Show file tree
Hide file tree
Showing 23 changed files with 1,527 additions and 142 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add and restructure tests for boolean and select components [#731]
- Add tests for `gate_add` and `gate_mul` [#736]
- Add and restructure tests for `component_decomposition` [#738]
- Add `Compile::encode` and `Compile::decode` [#752]

### Removed

Expand Down
8 changes: 4 additions & 4 deletions benches/plonk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,11 @@ fn run<const DEGREE: usize>(
Compiler::compile::<BenchCircuit<DEGREE>>(&pp, label)
.expect("failed to compile circuit");

let circuit: BenchCircuit<DEGREE> = BenchCircuit::default();

// sanity run
let (proof, public_inputs) = prover
.prove(&mut rand_core::OsRng, &Default::default())
.prove(&mut rand_core::OsRng, &circuit)
.expect("failed to prove");

verifier
Expand All @@ -103,9 +105,7 @@ fn run<const DEGREE: usize>(
let description = format!("Prove 2^{} = {} gates", power, DEGREE);

c.bench_function(description.as_str(), |b| {
b.iter(|| {
black_box(prover.prove(&mut rand_core::OsRng, &Default::default()))
})
b.iter(|| black_box(prover.prove(&mut rand_core::OsRng, &circuit)))
});

let description = format!("Verify 2^{} = {} gates", power, DEGREE);
Expand Down
4 changes: 2 additions & 2 deletions src/composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ pub trait Composer: Sized + Index<Witness, Output = BlsScalar> {
///
/// A turbo composer expects the first witness to be always present and to
/// be zero.
const ZERO: Witness = Witness::new(0);
const ZERO: Witness = Witness::ZERO;

/// `One` representation inside the constraint system.
///
/// A turbo composer expects the 2nd witness to be always present and to
/// be one.
const ONE: Witness = Witness::new(1);
const ONE: Witness = Witness::ONE;

/// Identity point representation inside the constraint system
const IDENTITY: WitnessPoint = WitnessPoint::new(Self::ZERO, Self::ONE);
Expand Down
269 changes: 250 additions & 19 deletions src/composer/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,24 @@
//
// Copyright (c) DUSK NETWORK. All rights reserved.

use core::{iter, mem};

use dusk_bls12_381::BlsScalar;

use crate::commitment_scheme::{CommitKey, OpeningKey, PublicParameters};
use crate::constraint_system::{Constraint, Selector, Witness};
use crate::error::Error;
use crate::fft::{EvaluationDomain, Evaluations, Polynomial as FftPolynomial};
use crate::proof_system::preprocess::Polynomials;
use crate::proof_system::{widget, ProverKey};

use super::{Builder, Circuit, Composer, Prover, Verifier};
use super::{Builder, Circuit, Composer, Polynomial, Prover, Verifier};

mod scalar;
use scalar::ScalarEncoder;

mod witness;
use witness::WitnessEncoder;

/// Generate the arguments to prove and verify a circuit
pub struct Compiler;
Expand All @@ -24,48 +33,267 @@ impl Compiler {
pub fn compile<C>(
pp: &PublicParameters,
label: &[u8],
) -> Result<(Prover<C>, Verifier<C>), Error>
) -> Result<(Prover, Verifier), Error>
where
C: Circuit,
{
Self::compile_with_circuit(pp, label, &Default::default())
let max_size = Self::max_size(pp);
let mut builder = Builder::initialized(max_size);

C::default().circuit(&mut builder)?;

Self::compile_with_builder(pp, label, &builder)
}

/// Create a new arguments set from a given circuit instance
/// Compile a circuit and encode its prover and verifier into the provided
/// buffer, extending its bytes.
///
/// Returns the amount of bytes written to `buffer`.
///
/// Use the provided circuit instead of the default implementation
pub fn compile_with_circuit<C>(
/// [Self::decode] is used to return the prover/verifier.
pub fn encode<C, B>(
pp: &PublicParameters,
label: &[u8],
circuit: &C,
) -> Result<(Prover<C>, Verifier<C>), Error>
buffer: &mut B,
) -> Result<usize, Error>
where
C: Circuit,
B: Extend<u8>,
{
let max_size = (pp.commit_key.powers_of_g.len() - 1) >> 1;
let mut prover = Builder::initialized(max_size);
let max_size = Self::max_size(pp);
let mut builder = Builder::initialized(max_size);
let encoder = ScalarEncoder::default();

C::default().circuit(&mut builder)?;
let mut n = 0;

let witnesses = (builder.witnesses.len() as u64).to_le_bytes();
n += witnesses.len();
buffer.extend(witnesses);

let constraints = (builder.constraints.len() as u64).to_le_bytes();
n += constraints.len();
buffer.extend(constraints);

// TODO encode public input indexes

n += builder.constraints.iter().enumerate().fold(
0,
|n, (i, polynomial)| {
let has_pi = builder.public_inputs.contains_key(&i);
buffer.extend(iter::once(has_pi as u8));
let Polynomial {
q_m,
q_l,
q_r,
q_o,
q_c,
q_d,
q_arith,
q_range,
q_logic,
q_fixed_group_add,
q_variable_group_add,
w_a,
w_b,
w_d,
w_o,
} = polynomial;
n + 1
+ encoder.encode(q_m, buffer)
+ encoder.encode(q_l, buffer)
+ encoder.encode(q_r, buffer)
+ encoder.encode(q_o, buffer)
+ encoder.encode(q_c, buffer)
+ encoder.encode(q_d, buffer)
+ encoder.encode(q_arith, buffer)
+ encoder.encode(q_range, buffer)
+ encoder.encode(q_logic, buffer)
+ encoder.encode(q_fixed_group_add, buffer)
+ encoder.encode(q_variable_group_add, buffer)
+ WitnessEncoder::encode(*w_a, buffer)
+ WitnessEncoder::encode(*w_b, buffer)
+ WitnessEncoder::encode(*w_d, buffer)
+ WitnessEncoder::encode(*w_o, buffer)
},
);

Ok(n)
}

/// Compile a circuit and encode its prover and verifier into the provided
/// buffer, extending its bytes.
///
/// It will call [Self::encode] with a pre-allocated [Vec] so it will not
/// reallocate during runtime.
pub fn encode_to_vec<C>(pp: &PublicParameters) -> Result<Vec<u8>, Error>
where
C: Circuit,
{
// pre-alloc to worst case scenario
let max_size = Self::max_size(pp);
let capacity = max_size * mem::size_of::<Constraint>();
let mut buffer = Vec::with_capacity(capacity);
Self::encode::<C, _>(pp, &mut buffer)?;
Ok(buffer)
}

/// Generates a [Prover] and [Verifier] from a buffer created by
/// [Self::encode].
///
/// Returns the amount of bytes read from the `buffer`.
pub fn decode(
pp: &PublicParameters,
label: &[u8],
mut buffer: &[u8],
) -> Result<(usize, Prover, Verifier), Error> {
if buffer.len() < 17 {
return Err(Error::NotEnoughBytes);
}

let mut witnesses = [0; 8];
let mut constraints = [0; 8];

witnesses.copy_from_slice(&buffer[..8]);
constraints.copy_from_slice(&buffer[8..16]);

let witnesses = u64::from_le_bytes(witnesses);
let constraints = u64::from_le_bytes(constraints);

// TODO decode public input indexes

circuit.circuit(&mut prover)?;
let mut n = 16;

let n = (prover.constraints() + 6).next_power_of_two();
buffer = &buffer[n..];

#[allow(deprecated)]
let mut builder = Builder::uninitialized(constraints as usize);

(0..witnesses).for_each(|_| {
builder.append_witness(BlsScalar::zero());
});

(0..constraints).try_for_each::<_, Result<_, Error>>(|_| {
if buffer.is_empty() {
return Err(Error::NotEnoughBytes);
}
let has_pi = buffer[0] == true as u8;
n += 1;
buffer = &buffer[1..];

// decode the selectors
let q_m = Self::decode_scalar(&mut buffer, &mut n)?;
let q_l = Self::decode_scalar(&mut buffer, &mut n)?;
let q_r = Self::decode_scalar(&mut buffer, &mut n)?;
let q_o = Self::decode_scalar(&mut buffer, &mut n)?;
let q_c = Self::decode_scalar(&mut buffer, &mut n)?;
let q_d = Self::decode_scalar(&mut buffer, &mut n)?;
let q_arith = Self::decode_scalar(&mut buffer, &mut n)?;
let q_range = Self::decode_scalar(&mut buffer, &mut n)?;
let q_logic = Self::decode_scalar(&mut buffer, &mut n)?;
let q_fixed_group_add = Self::decode_scalar(&mut buffer, &mut n)?;
let q_variable_group_add =
Self::decode_scalar(&mut buffer, &mut n)?;

// decode the witnesses
let w_a = Self::decode_witness(&mut buffer, &mut n)?;
let w_b = Self::decode_witness(&mut buffer, &mut n)?;
let w_d = Self::decode_witness(&mut buffer, &mut n)?;
let w_o = Self::decode_witness(&mut buffer, &mut n)?;

// setup the constraint
let mut constraint = Constraint::default()
.set(Selector::Multiplication, q_m)
.set(Selector::Left, q_l)
.set(Selector::Right, q_r)
.set(Selector::Output, q_o)
.set(Selector::Constant, q_c)
.set(Selector::Fourth, q_d)
.set(Selector::Arithmetic, q_arith)
.set(Selector::Range, q_range)
.set(Selector::Logic, q_logic)
.set(Selector::GroupAddFixedBase, q_fixed_group_add)
.set(Selector::GroupAddVariableBase, q_variable_group_add)
.a(w_a)
.b(w_b)
.d(w_d)
.o(w_o);

// append a PI, if applicable
if has_pi {
constraint = constraint.public(BlsScalar::zero());
}

// append the constraint to the builder
builder.append_custom_gate(constraint);

Ok(())
})?;

let (prover, verifier) =
Self::compile_with_builder(pp, label, &builder)?;

Ok((n, prover, verifier))
}

/// Consumes a buffer to decode a scalar. Update the buffer to its
/// remainder.
fn decode_scalar(
buffer: &mut &[u8],
n: &mut usize,
) -> Result<BlsScalar, Error> {
let (consumed, scalar) = ScalarEncoder::decode(*buffer);
if consumed == 0 {
return Err(Error::BlsScalarMalformed);
}
*n += consumed;
*buffer = &(*buffer)[consumed..];
Ok(scalar)
}

/// Consumes a buffer to decode a witness. Update the buffer to its
/// remainder.
fn decode_witness(
buffer: &mut &[u8],
n: &mut usize,
) -> Result<Witness, Error> {
let (consumed, witness) = WitnessEncoder::decode(*buffer);
if consumed == 0 {
return Err(Error::BlsScalarMalformed);
}
*n += consumed;
*buffer = &(*buffer)[consumed..];
Ok(witness)
}

/// Returns the maximum constraints length for the parameters.
fn max_size(pp: &PublicParameters) -> usize {
(pp.commit_key.powers_of_g.len() - 1) >> 1
}

/// Create a new arguments set from a given circuit instance
///
/// Use the default implementation of the circuit
fn compile_with_builder(
pp: &PublicParameters,
label: &[u8],
builder: &Builder,
) -> Result<(Prover, Verifier), Error> {
let n = (builder.constraints() + 6).next_power_of_two();

let (commit, opening) = pp.trim(n)?;

let (prover, verifier) =
Self::preprocess(label, commit, opening, &prover)?;
Self::preprocess(label, commit, opening, &builder)?;

Ok((prover, verifier))
}

fn preprocess<C>(
fn preprocess(
label: &[u8],
commit_key: CommitKey,
opening_key: OpeningKey,
prover: &Builder,
) -> Result<(Prover<C>, Verifier<C>), Error>
where
C: Circuit,
{
) -> Result<(Prover, Verifier), Error> {
let mut perm = prover.perm.clone();

let constraints = prover.constraints();
Expand Down Expand Up @@ -386,3 +614,6 @@ impl Compiler {
Ok((prover, verifier))
}
}

#[test]
fn compiler_encode_and_decode() {}
Loading

0 comments on commit 7f9d305

Please sign in to comment.