From 20db596e573e7f9d78af6010b5b2518c9a978b02 Mon Sep 17 00:00:00 2001 From: Robin Salen Date: Fri, 5 Jan 2024 09:45:45 +0100 Subject: [PATCH] Add some more explicit doc on plonky2 crate --- plonky2/src/gadgets/mod.rs | 3 + plonky2/src/gates/gate.rs | 49 +++++++++++- plonky2/src/iop/ext_target.rs | 4 + plonky2/src/iop/target.rs | 16 +++- plonky2/src/plonk/circuit_builder.rs | 114 ++++++++++++++++++++++++--- plonky2/src/plonk/circuit_data.rs | 12 +++ 6 files changed, 185 insertions(+), 13 deletions(-) diff --git a/plonky2/src/gadgets/mod.rs b/plonky2/src/gadgets/mod.rs index 9016211f..ba19e667 100644 --- a/plonky2/src/gadgets/mod.rs +++ b/plonky2/src/gadgets/mod.rs @@ -1,3 +1,6 @@ +//! Gadgets provide additional methods to [`CircuitBuilder`] +//! to ease circuit creation. + pub mod arithmetic; pub mod arithmetic_extension; pub mod hash; diff --git a/plonky2/src/gates/gate.rs b/plonky2/src/gates/gate.rs index 2f8f7df1..baf26f8c 100644 --- a/plonky2/src/gates/gate.rs +++ b/plonky2/src/gates/gate.rs @@ -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, 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, common_data: &CommonCircuitData) -> IoResult<()>; + /// Deserializes the bytes in the provided buffer into this custom gate, given some [`CommonCircuitData`]. fn deserialize(src: &mut Buffer, common_data: &CommonCircuitData) -> IoResult 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) -> Vec; /// Like `eval_unfiltered`, but specialized for points in the base field. @@ -88,6 +118,12 @@ pub trait Gate, 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, @@ -175,10 +211,20 @@ pub trait Gate, 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>; /// 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, 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. diff --git a/plonky2/src/iop/ext_target.rs b/plonky2/src/iop/ext_target.rs index 68d81fef..c64d96e8 100644 --- a/plonky2/src/iop/ext_target.rs +++ b/plonky2/src/iop/ext_target.rs @@ -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(pub [Target; D]); diff --git a/plonky2/src/iop/target.rs b/plonky2/src/iop/target.rs index c0a1e286..f1225714 100644 --- a/plonky2/src/iop/target.rs +++ b/plonky2/src/iop/target.rs @@ -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 { diff --git a/plonky2/src/plonk/circuit_builder.rs b/plonky2/src/plonk/circuit_builder.rs index 3bc34099..38281d51 100644 --- a/plonky2/src/plonk/circuit_builder.rs +++ b/plonky2/src/plonk/circuit_builder.rs @@ -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 = >::F; +/// +/// let config = CircuitConfig::standard_recursion_config(); +/// let mut builder = CircuitBuilder::::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::(); +/// +/// // 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, 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, const D: usize> { } impl, const D: usize> CircuitBuilder { + /// 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, const D: usize> CircuitBuilder { 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, const D: usize> CircuitBuilder { 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, const D: usize> CircuitBuilder { 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, const D: usize> CircuitBuilder { 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, const D: usize> CircuitBuilder { Target::VirtualTarget { index } } + /// Adds `n` new "virtual" targets. pub fn add_virtual_targets(&mut self, n: usize) -> Vec { (0..n).map(|_i| self.add_virtual_target()).collect() } + /// Adds `N` new "virtual" targets, arranged as an array. pub fn add_virtual_target_arr(&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 { (0..n).map(|_i| self.add_virtual_hash()).collect() } @@ -337,7 +403,9 @@ impl, const D: usize> CircuitBuilder { } /// 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, const D: usize> CircuitBuilder { ); } + /// 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) { self.gates.insert(gate); } - pub fn connect_extension(&mut self, src: ExtensionTarget, dst: ExtensionTarget) { - 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, const D: usize> CircuitBuilder { /// 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, const D: usize> CircuitBuilder { .push(CopyConstraint::new((x, y), self.context_log.open_stack())); } + /// Enforces that two [`ExtensionTarget`] underlying values are equal. + pub fn connect_extension(&mut self, src: ExtensionTarget, dst: ExtensionTarget) { + 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>) { + fn add_generators(&mut self, generators: Vec>) { self.generators.extend(generators); } @@ -479,10 +568,12 @@ impl, const D: usize> CircuitBuilder { 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, const D: usize> CircuitBuilder { target } + /// Returns a vector of routable targets with the given constant values. pub fn constants(&mut self, constants: &[F]) -> Vec { 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, const D: usize> CircuitBuilder { } } + /// Returns a routable [`HashOutTarget`]. pub fn constant_hash(&mut self, h: HashOut) -> HashOutTarget { HashOutTarget { elements: h.elements.map(|x| self.constant(x)), } } + /// Returns a routable [`MerkleCapTarget`]. pub fn constant_merkle_cap>>( &mut self, cap: &MerkleCap, @@ -545,7 +640,7 @@ impl, const D: usize> CircuitBuilder { 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) -> Option { // Get a Vec of any coefficients that are constant. If we end up with exactly D of them, @@ -1178,6 +1273,7 @@ impl, const D: usize> CircuitBuilder { ) } + /// Builds a "full circuit", with both prover and verifier data. pub fn build>(self) -> CircuitData { self.build_with_options(true) } diff --git a/plonky2/src/plonk/circuit_data.rs b/plonky2/src/plonk/circuit_data.rs index 1b07a8b2..c0c60220 100644 --- a/plonky2/src/plonk/circuit_data.rs +++ b/plonky2/src/plonk/circuit_data.rs @@ -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.