Add some more explicit doc on plonky2 crate

This commit is contained in:
Robin Salen 2024-01-05 09:45:45 +01:00
parent f46cf4ef68
commit 20db596e57
No known key found for this signature in database
GPG Key ID: F98FD38F65687358
6 changed files with 185 additions and 13 deletions

View File

@ -1,3 +1,6 @@
//! Gadgets provide additional methods to [`CircuitBuilder`]
//! to ease circuit creation.
pub mod arithmetic;
pub mod arithmetic_extension;
pub mod hash;

View File

@ -26,15 +26,45 @@ use crate::plonk::vars::{
use crate::util::serialization::{Buffer, IoResult};
/// A custom gate.
///
/// Vanilla Plonk arithmetization only supports basic fan-in 2 / fan-out 1 arithmetic gates,
/// each of the form
///
/// $$ a.b.q_M + a.q_L + b.q_R + c.q_O + q_C = 0 $$
///
/// where:
/// - q_M, q_L, q_R and q_O are boolean selectors,
/// - a, b and c are values used as inputs and output respectively,
/// - q_C is a constant (possibly 0).
///
/// This allows expressing simple operations like multiplication, addition, etc. For
/// instance, to define a multiplication, one can set q_M=1, q_L=q_R=0, q_O = -1 and q_C = 0.
/// Hence, the gate equation simplifies to a.b - c = 0, or a.b = c.
///
/// However, such gate is fairly limited for more complex computations. Hence, when a computation may
/// require too many of these "vanilla" gates, or when a computation arises often within the same circuit,
/// one may want to construct a tailored custom gate. These custom gates can use more selectors and are
/// not necessarily limited to 2 inputs + 1 output = 3 wires.
/// For instance, plonky2 supports natively a custom Poseidon hash gate that uses 135 wires.
///
/// Note however that extending the number of wires necessary for a custom gate comes at a price, and may
/// impact the overall performances when generating proofs for a circuit containing them.
pub trait Gate<F: RichField + Extendable<D>, const D: usize>: 'static + Send + Sync {
/// Defines a unique identifier for this custom gate.
///
/// This is used as differentiating tag in gate serializers.
fn id(&self) -> String;
/// Serializes this custom gate to the targeted byte buffer, with the provided [`CommonCircuitData`].
fn serialize(&self, dst: &mut Vec<u8>, common_data: &CommonCircuitData<F, D>) -> IoResult<()>;
/// Deserializes the bytes in the provided buffer into this custom gate, given some [`CommonCircuitData`].
fn deserialize(src: &mut Buffer, common_data: &CommonCircuitData<F, D>) -> IoResult<Self>
where
Self: Sized;
/// Defines the constraints that enforce the statement represented by this gate.
/// Constraints must be defined in the extension of this custom gate base field.
fn eval_unfiltered(&self, vars: EvaluationVars<F, D>) -> Vec<F::Extension>;
/// Like `eval_unfiltered`, but specialized for points in the base field.
@ -88,6 +118,12 @@ pub trait Gate<F: RichField + Extendable<D>, const D: usize>: 'static + Send + S
res
}
/// Defines the recursive constraints that enforce the statement represented by this custom gate.
/// This is necessary to recursively verify proofs generated from a circuit containing such gates.
///
/// **Note**: The order of the recursive constraints output by this method should match exactly the order
/// of the constraints obtained by the non-recursive [`Gate::eval_unfiltered`] method, otherwise the
/// prover won't be able to generate proofs.
fn eval_unfiltered_circuit(
&self,
builder: &mut CircuitBuilder<F, D>,
@ -175,10 +211,20 @@ pub trait Gate<F: RichField + Extendable<D>, const D: usize>: 'static + Send + S
}
/// The generators used to populate the witness.
/// Note: This should return exactly 1 generator per operation in the gate.
///
/// **Note**: This should return exactly 1 generator per operation in the gate.
fn generators(&self, row: usize, local_constants: &[F]) -> Vec<WitnessGeneratorRef<F, D>>;
/// The number of wires used by this gate.
///
/// While vanilla Plonk can only evaluate one addition/multiplication at a time, a wider
/// configuration may be able to accomodate several identical gates at once. This is
/// particularly helpful for tiny custom gates that are being used extensively in circuits.
///
/// For instance, the [crate::gates::multiplication_extension::MulExtensionGate] takes `3*D`
/// wires per multiplication (where `D`` is the degree of the extension), hence for a usual
/// configuration of 80 routed wires with D=2, one can evaluate 13 multiplications within a
/// single gate.
fn num_wires(&self) -> usize;
/// The number of constants used by this gate.
@ -187,6 +233,7 @@ pub trait Gate<F: RichField + Extendable<D>, const D: usize>: 'static + Send + S
/// The maximum degree among this gate's constraint polynomials.
fn degree(&self) -> usize;
/// The number of constraints defined by this sole custom gate.
fn num_constraints(&self) -> usize;
/// Number of operations performed by the gate.

View File

@ -9,6 +9,10 @@ use crate::iop::target::Target;
use crate::plonk::circuit_builder::CircuitBuilder;
/// `Target`s representing an element of an extension field.
///
/// This is typically used in recursion settings, where the outer circuit must verify
/// a proof satisfying an inner circuit's statement, which is verified using arithmetic
/// in an extension of the base field.
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct ExtensionTarget<const D: usize>(pub [Target; D]);

View File

@ -8,15 +8,25 @@ use crate::iop::wire::Wire;
use crate::plonk::circuit_data::CircuitConfig;
/// A location in the witness.
///
/// Targets can either be placed at a specific location, or be "floating" around,
/// serving as intermediary value holders, and copied to other locations whenever needed.
///
/// When generating a proof for a given circuit, the prover will "set" the values of some
/// (or all) targets, so that they satisfy the circuit constraints. This is done through
/// the [`PartialWitness`] interface.
///
/// There are different "variants" of the `Target` type, namely [`ExtensionTarget`],
/// [`ExtensionAlgebraTarget`], but the `Target` type is the default one for most circuits
/// verifying some simple statement.
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
pub enum Target {
/// A target that has a fixed location in the witness (seen as a `degree x num_wires` grid).
Wire(Wire),
/// A target that doesn't have any inherent location in the witness (but it can be copied to
/// another target that does). This is useful for representing intermediate values in witness
/// generation.
VirtualTarget {
index: usize,
},
VirtualTarget { index: usize },
}
impl Default for Target {

View File

@ -83,7 +83,56 @@ pub struct LookupWire {
/// Index of the first lookup table row (i.e. the last `LookupTableGate`).
pub first_lut_gate: usize,
}
/// Structure used to construct a plonky2 circuit. It provides all the necessary toolkit that,
/// from an initial circuit configuration, will enable one to design a circuit and its associated
/// prover/verifier data.
///
/// # Usage
///
/// ```rust
/// use plonky2::plonk::circuit_data::CircuitConfig;
/// use plonky2::plonk::circuit_builder::CircuitBuilder;
/// use plonky2::plonk::config::PoseidonGoldilocksConfig;
///
/// // Define parameters for this circuit
/// const D: usize = 2;
/// type C = PoseidonGoldilocksConfig;
/// type F = <C as GenericConfig<D>>::F;
///
/// let config = CircuitConfig::standard_recursion_config();
/// let mut builder = CircuitBuilder::<F, D>::new(config);
///
/// // Build a circuit for the statement: "I know the 100th term
/// // of the Fibonacci sequence, starting from 0 and 1".
/// let initial_a = builder.constant(F::ZERO);
/// let initial_b = builder.constant(F::ONE);
/// let mut prev_target = initial_a;
/// let mut cur_target = initial_b;
/// for _ in 0..99 {
/// let temp = builder.add(prev_target, cur_target);
/// prev_target = cur_target;
/// cur_target = temp;
/// }
///
/// // The only public input is the result (which is generated).
/// builder.register_public_input(cur_target);
///
/// // Build the circuit
/// let circuit_data = builder.build::<C>();
///
/// // Now compute the witness and generate a proof
/// let mut pw = PartialWitness::new();
///
/// // There are no public inputs to register, as the only one
/// // will be generated while proving the statement.
/// let proof = data.prove(pw).unwrap();
///
/// // Verify the proof
/// assert!(data.verify(proof).is_ok());
/// ```
pub struct CircuitBuilder<F: RichField + Extendable<D>, const D: usize> {
/// Circuit configuration to be used by this `CircuitBuilder`.
pub config: CircuitConfig,
/// A domain separator, which is included in the initial Fiat-Shamir seed. This is generally not
@ -146,6 +195,10 @@ pub struct CircuitBuilder<F: RichField + Extendable<D>, const D: usize> {
}
impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
/// Given a [`CircuitConfig`], generate a new [`CircuitBuilder`] instance.
/// It will also check that the configuration provided is consistent, i.e.
/// that the different parameters provided can achieve the targeted security
/// level.
pub fn new(config: CircuitConfig) -> Self {
let builder = CircuitBuilder {
config,
@ -173,6 +226,8 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
builder
}
/// Assert that the configuration used to create this `CircuitBuilder` is consistent,
/// i.e. that the different parameters meet the targeted security level.
fn check_config(&self) {
let &CircuitConfig {
security_bits,
@ -201,6 +256,7 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
self.domain_separator = Some(separator);
}
/// Outputs the number of gates in this circuit.
pub fn num_gates(&self) -> usize {
self.gate_instances.len()
}
@ -215,6 +271,7 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
targets.iter().for_each(|&t| self.register_public_input(t));
}
/// Outputs the number of public inputs in this circuit.
pub fn num_public_inputs(&self) -> usize {
self.public_inputs.len()
}
@ -244,10 +301,13 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
self.lut_to_lookups[lut_index].push((looking_in, looking_out));
}
/// Outputs the number of lookup tables in this circuit.
pub fn num_luts(&self) -> usize {
self.lut_to_lookups.len()
}
/// Given an index, outputs the corresponding looking table in the set of tables
/// used in this circuit, as a sequence of target tuples `(input, output)`.
pub fn get_lut_lookups(&self, lut_index: usize) -> &[(Target, Target)] {
&self.lut_to_lookups[lut_index]
}
@ -262,22 +322,28 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
Target::VirtualTarget { index }
}
/// Adds `n` new "virtual" targets.
pub fn add_virtual_targets(&mut self, n: usize) -> Vec<Target> {
(0..n).map(|_i| self.add_virtual_target()).collect()
}
/// Adds `N` new "virtual" targets, arranged as an array.
pub fn add_virtual_target_arr<const N: usize>(&mut self) -> [Target; N] {
[0; N].map(|_| self.add_virtual_target())
}
/// Adds a new `HashOutTarget`. `NUM_HASH_OUT_ELTS` being hardcoded to 4, it internally
/// adds 4 virtual targets in a vector fashion.
pub fn add_virtual_hash(&mut self) -> HashOutTarget {
HashOutTarget::from_vec(self.add_virtual_targets(4))
}
/// Adds a new `MerkleCapTarget`, consisting in `1 << cap_height` `HashOutTarget`.
pub fn add_virtual_cap(&mut self, cap_height: usize) -> MerkleCapTarget {
MerkleCapTarget(self.add_virtual_hashes(1 << cap_height))
}
/// Adds `n` new `HashOutTarget` in a vector fashion.
pub fn add_virtual_hashes(&mut self, n: usize) -> Vec<HashOutTarget> {
(0..n).map(|_i| self.add_virtual_hash()).collect()
}
@ -337,7 +403,9 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
}
/// Add a virtual verifier data, register it as a public input and set it to `self.verifier_data_public_input`.
/// WARNING: Do not register any public input after calling this! TODO: relax this
///
/// **WARNING**: Do not register any public input after calling this!
// TODO: relax this
pub fn add_verifier_data_public_inputs(&mut self) -> VerifierCircuitTarget {
assert!(
self.verifier_data_public_input.is_none(),
@ -410,16 +478,12 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
);
}
/// Adds a gate type to the set of gates to be used in this circuit. This can be useful
/// in conditional recursion to uniformize the gates set of the different circuits.
pub fn add_gate_to_gate_set(&mut self, gate: GateRef<F, D>) {
self.gates.insert(gate);
}
pub fn connect_extension(&mut self, src: ExtensionTarget<D>, dst: ExtensionTarget<D>) {
for i in 0..D {
self.connect(src.0[i], dst.0[i]);
}
}
/// Adds a generator which will copy `src` to `dst`.
pub fn generate_copy(&mut self, src: Target, dst: Target) {
self.add_simple_generator(CopyGenerator { src, dst });
@ -427,6 +491,8 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
/// Uses Plonk's permutation argument to require that two elements be equal.
/// Both elements must be routable, otherwise this method will panic.
///
/// For an example of usage, see [`CircuitBuilder::assert_one()`].
pub fn connect(&mut self, x: Target, y: Target) {
assert!(
x.is_routable(&self.config),
@ -440,17 +506,40 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
.push(CopyConstraint::new((x, y), self.context_log.open_stack()));
}
/// Enforces that two [`ExtensionTarget<D>`] underlying values are equal.
pub fn connect_extension(&mut self, src: ExtensionTarget<D>, dst: ExtensionTarget<D>) {
for i in 0..D {
self.connect(src.0[i], dst.0[i]);
}
}
/// Enforces that a routable `Target` value is 0, using Plonk's permutation argument.
pub fn assert_zero(&mut self, x: Target) {
let zero = self.zero();
self.connect(x, zero);
}
/// Enforces that a routable `Target` value is 1, using Plonk's permutation argument.
///
/// # Example
///
/// Let say the circuit contains a target `a`, and a target `b` as public input so that the
/// prover can non-deterministically compute the multiplicative inverse of `a` when generating
/// a proof.
///
/// One can then add the following constraint in the circuit to enforce that the value provided
/// by the prover is correct:
///
/// ```ignore
/// let c = builder.mul(a, b);
/// builder.assert_one(c);
/// ```
pub fn assert_one(&mut self, x: Target) {
let one = self.one();
self.connect(x, one);
}
pub fn add_generators(&mut self, generators: Vec<WitnessGeneratorRef<F, D>>) {
fn add_generators(&mut self, generators: Vec<WitnessGeneratorRef<F, D>>) {
self.generators.extend(generators);
}
@ -479,10 +568,12 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
self.constant(F::NEG_ONE)
}
/// Returns a rootable boolean target set to false.
pub fn _false(&mut self) -> BoolTarget {
BoolTarget::new_unsafe(self.zero())
}
/// Returns a rootable boolean target set to true.
pub fn _true(&mut self) -> BoolTarget {
BoolTarget::new_unsafe(self.one())
}
@ -501,10 +592,12 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
target
}
/// Returns a vector of routable targets with the given constant values.
pub fn constants(&mut self, constants: &[F]) -> Vec<Target> {
constants.iter().map(|&c| self.constant(c)).collect()
}
/// Returns a routable target with the given constant boolean value.
pub fn constant_bool(&mut self, b: bool) -> BoolTarget {
if b {
self._true()
@ -513,12 +606,14 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
}
}
/// Returns a routable [`HashOutTarget`].
pub fn constant_hash(&mut self, h: HashOut<F>) -> HashOutTarget {
HashOutTarget {
elements: h.elements.map(|x| self.constant(x)),
}
}
/// Returns a routable [`MerkleCapTarget`].
pub fn constant_merkle_cap<H: Hasher<F, Hash = HashOut<F>>>(
&mut self,
cap: &MerkleCap<F, H>,
@ -545,7 +640,7 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
self.targets_to_constants.get(&target).cloned()
}
/// If the given `ExtensionTarget` is a constant (i.e. it was created by the
/// If the given [`ExtensionTarget`] is a constant (i.e. it was created by the
/// `constant_extension(F)` method), returns its constant value. Otherwise, returns `None`.
pub fn target_as_constant_ext(&self, target: ExtensionTarget<D>) -> Option<F::Extension> {
// Get a Vec of any coefficients that are constant. If we end up with exactly D of them,
@ -1178,6 +1273,7 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
)
}
/// Builds a "full circuit", with both prover and verifier data.
pub fn build<C: GenericConfig<D, F = F>>(self) -> CircuitData<F, C, D> {
self.build_with_options(true)
}

View File

@ -38,9 +38,19 @@ use crate::util::serialization::{
};
use crate::util::timing::TimingTree;
/// Configuration to be used when building a circuit. This defines the shape of the circuit
/// as well as its targeted security level and sub-protocol (e.g. FRI) parameters.
///
/// It supports a [`Default`] implementation tailored for recursion with Poseidon hash (of width 12)
/// as internal hash function and FRI rate of 1/8.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct CircuitConfig {
/// The number of wires available at each row. This corresponds to the "width" of the circuit,
/// and consists in the sum of routed wires and advice wires.
pub num_wires: usize,
/// The number of routed wires, i.e. wires that will be involved in Plonk's permutation argument.
/// This allows copy constraints, i.e. enforcing that two distant values in a circuit are equal.
/// Non-routed wires are called advice wires.
pub num_routed_wires: usize,
pub num_constants: usize,
/// Whether to use a dedicated gate for base field arithmetic, rather than using a single gate
@ -50,6 +60,8 @@ pub struct CircuitConfig {
/// The number of challenge points to generate, for IOPs that have soundness errors of (roughly)
/// `degree / |F|`.
pub num_challenges: usize,
/// A boolean to activate the zero-knowledge property. When this is set to `false`, proofs *may*
/// leak additional information.
pub zero_knowledge: bool,
/// A cap on the quotient polynomial's degree factor. The actual degree factor is derived
/// systematically, but will never exceed this value.