Gadgets

Gadgets in ZKPs are analogous to functions in programming languages. Like functions, they can be invoked from within a ZKP, take inputs, and compute outputs. Gadgets can also call other gadgets.

We refer to the inputs of the gadget as "gadget inputs". With these inputs, the gadget then computes "hidden inputs" which will likely (but not always) be the gadget outputs. They're "hidden" in the sense that they're not directly passed into prove or verify but instead snuck into the R1CS constraints outside of the argument list; they're "inputs" in the sense that they will be viewed as private values that will be fed into your ZKP program. It is up to the gadget implementation to prove that the hidden inputs are calculated correctly.

Why you might want to create a gadget

There a few reasons you might be interested in creating a gadget:

  • To fine-tune/improve performance
  • To re-use them across different ZKP programs
  • To accomplish things not native to R1CS like division

How to create a gadget

You can create your own gadgets by implementing the Gadget trait:1

#![allow(unused)]
fn main() {
use sunscreen::{Result, types::zkp::{BigInt, NodeIndex}};
use std::any::Any;
pub trait Gadget: Any + Send + Sync {
    /// Generate the circuit that proves the hidden inputs are calculated correctly
    /// and return the gadget outputs.
    fn gen_circuit(
        &self,
        gadget_inputs: &[NodeIndex],
        hidden_inputs: &[NodeIndex],
    ) -> Vec<NodeIndex>;

    /// Compute the ZKP hidden inputs from the gadget inputs
    fn compute_hidden_inputs(&self, gadget_inputs: &[BigInt]) -> Result<Vec<BigInt>>;

    /// Expected # of gadget inputs (this should be constant for a given implementation)
    fn gadget_input_count(&self) -> usize;

    /// Expected # of ZKP hidden inputs/gadget outputs (this should be constant for a given implementation)
    fn hidden_input_count(&self) -> usize;
}
}

You may have noticed that the Field<F> type does not occur in the gadget definition. This is mostly an implementation detail, as we need to package up various gadgets as dynamic trait objects. Instead, you work with BigInts which are the internal representation of a native field. If your gadget relies on any context specific to the ZKP backend, you'll have to provide that context to the gadget. This is demonstrated in the example below.

Warning: Even though we're working with BigInt values, the gadget must return values less than the field modulus otherwise proof generation will fail. You can pass the field modulus as an argument to the gadget's constructor and provide this value via FieldSpec::FIELD_MODULUS. See example.

Example

Let's walk through an example of a gadget that computes the multiplicative inverse of a native field element. Here, the gadget input will be some native field element \(x\) and the calculated hidden input will be \(x^{-1}\). The gadget output is just the hidden input in this case.2

Definition

The inverse calculation is dependent on the field modulus, so we'll parameterize the gadget with this context:

#![allow(unused)]
fn main() {
use sunscreen::types::zkp::BigInt;

pub struct Inverse {
    field_modulus: BigInt,
}
}

Counting inputs

Let's get the easy stuff out of the way. To calculate an inverse, we take in one native field element (this will be the value to be inverted) and return one native field element (this will be the inverse). Thus:

#![allow(unused)]
fn main() {
use sunscreen::{
    types::zkp::{BigInt, Gadget, NodeIndex},
    ZkpResult,
};
pub struct Inverse {
    field_modulus: BigInt,
}
impl Gadget for Inverse {

    fn gadget_input_count(&self) -> usize {
        1
    }

    fn hidden_input_count(&self) -> usize {
        1
    }

    fn compute_hidden_inputs(&self, gadget_inputs: &[BigInt]) -> ZkpResult<Vec<BigInt>> {
        todo!()
    }

    fn gen_circuit(
        &self,
        gadget_inputs: &[NodeIndex],
        hidden_inputs: &[NodeIndex],
    ) -> Vec<NodeIndex> {
        todo!()
    }
}
}

Computing inputs

Next, we compute the hidden input, which in this case is the inverse of the single gadget input. There's one very important detail to keep in mind when computing hidden inputs—to prevent timing (side channel) attacks, your implementation must run in constant time!

#![allow(unused)]
fn main() {
use sunscreen::{
    types::zkp::{BigInt, Gadget, NodeIndex},
    ZkpError, ZkpResult,
};
pub struct Inverse {
    field_modulus: BigInt,
}
impl Gadget for Inverse {

    fn gadget_input_count(&self) -> usize {
        1
    }

    fn hidden_input_count(&self) -> usize {
        1
    }

fn compute_hidden_inputs(&self, gadget_inputs: &[BigInt]) -> ZkpResult<Vec<BigInt>> {
    let x = gadget_inputs[0];

    if x == BigInt::ZERO {
        return Err(ZkpError::gadget_error("Cannot take inverse of zero."));
    }
    if self.field_modulus == BigInt::ZERO {
        return Err(ZkpError::gadget_error(
            "Cannot have a finite field of zero size.",
        ));
    }

    // Note: BigInt::inverse_fp runs in constant time
    let x_inv = x.inverse_fp(&self.field_modulus);

    Ok(vec![x_inv])
}

    fn gen_circuit(
        &self,
        gadget_inputs: &[NodeIndex],
        hidden_inputs: &[NodeIndex],
    ) -> Vec<NodeIndex> {
        todo!()
    }
}
}

Generating the circuit

Finally, we need to produce the circuit proving that the hidden inputs were calculated correctly.

#![allow(unused)]
fn main() {
use sunscreen::{
    types::zkp::{BigInt, Gadget, NodeIndex},
    zkp::{ZkpContextOps, with_zkp_ctx},
    ZkpError, ZkpResult,
};
pub struct Inverse {
    field_modulus: BigInt,
}
impl Gadget for Inverse {

    fn gadget_input_count(&self) -> usize {
        1
    }

    fn hidden_input_count(&self) -> usize {
        1
    }

    fn compute_hidden_inputs(&self, gadget_inputs: &[BigInt]) -> ZkpResult<Vec<BigInt>> {
        todo!()
    }

fn gen_circuit(
    &self,
    gadget_inputs: &[NodeIndex],
    hidden_inputs: &[NodeIndex],
) -> Vec<NodeIndex> {
   let x = gadget_inputs[0];
   let x_inv = hidden_inputs[0];

   with_zkp_ctx(|ctx| {
       // Assert x * x^-1 == 1
       let prod = ctx.add_multiplication(x, x_inv);
       ctx.add_constraint(prod, &BigInt::ONE);
   });

   vec![x_inv]
}
}
}

The proof that the inverse was calculated properly is quite simple in this case; we just need to constrain that the product of the gadget input and its inverse is equal to one. The with_zkp_ctx call is how we insert these constraints into the graph context. Finally, we return the inverse as the gadget output.

Invoking the gadget

To use our gadget, we use the sunscreen::invoke_gadget function. This function returns raw graph node indices, so library authors are encouraged to write a wrapper function for use in ZKP program functions.

#![allow(unused)]
fn main() {
use sunscreen::{
    invoke_gadget,
    types::zkp::{BigInt, Gadget, Field, FieldSpec, NodeIndex, ProgramNode},
    zkp::{ZkpContextOps, with_zkp_ctx},
    zkp_program, zkp_var, ZkpError, ZkpResult,
};
pub struct Inverse {
    field_modulus: BigInt,
}
impl Gadget for Inverse {
    fn gadget_input_count(&self) -> usize {
        1
    }
    fn hidden_input_count(&self) -> usize {
        1
    }
    fn compute_hidden_inputs(&self, gadget_inputs: &[BigInt]) -> ZkpResult<Vec<BigInt>> {
        let x = gadget_inputs[0];
    
        if x == BigInt::ZERO {
            return Err(ZkpError::gadget_error("Cannot take inverse of zero."));
        }
        if self.field_modulus == BigInt::ZERO {
            return Err(ZkpError::gadget_error(
                "Cannot have a finite field of zero size.",
            ));
        }
    
        // Note: BigInt::inverse_fp runs in constant time
        let x_inv = x.inverse_fp(&self.field_modulus);
    
        Ok(vec![x_inv])
    }
    fn gen_circuit(
        &self,
        gadget_inputs: &[NodeIndex],
        hidden_inputs: &[NodeIndex],
    ) -> Vec<NodeIndex> {
       let x = gadget_inputs[0];
       let x_inv = hidden_inputs[0];
    
       with_zkp_ctx(|ctx| {
           // Assert x * x^-1 == 1
           let prod = ctx.add_multiplication(x, x_inv);
           ctx.add_constraint(prod, &BigInt::ONE);
       });
    
       vec![x_inv]
    }
}
pub fn inverse<F: FieldSpec>(
    x: ProgramNode<Field<F>>
) -> ProgramNode<Field<F>> {
    let gadget = Inverse { field_modulus: F::FIELD_MODULUS };
    let x_inv_ids = invoke_gadget(gadget, x.ids);
    ProgramNode::new(&x_inv_ids)
}

#[zkp_program]
fn use_inverse<F: FieldSpec>(a: Field<F>) {
    let a_inv = inverse(a);
    let one = zkp_var!(1);
    (a * a_inv).constrain_eq(one);
}
}

Gadgets are a very useful tool for building up functionality to be reused across ZKPs. However, because hidden input calculations need to be constant time, gadgets need to be written very carefully.

If you'd like to see a real example of the utility of gadgets, check out the source of how Field::constrain_ge_bounded is implemented.

1

For those familiar with Rust, you might wonder why we're not using something like associated constants for the gadget and hidden input counts; the reason is that we need to ensure that your gadgets are object safe.

2

We mentioned above that gadget outputs are typically just the computed hidden inputs. For an example where this is not the case, suppose a gadget does integer division \( \lfloor a / b \rfloor \). The natural way to do this would be to compute hidden inputs \(q\) and \(r\) and generate a circuit proving that \(a = qb + r\) and \(0 \le r < b\). The gadget output, however, would just be the one hidden input quotient \(q\).