Constant inputs

In general, we recommend sticking with #[private] and #[public] argument types.

Why you might want to use constant inputs

However, if you find that you're using a fixed public value as a ZKP program input, you might want to consider using the #[constant] argument type instead of #[public]. Often times, this will give you a performance boost (you'll see why below).

However, there is a trade-off in that constant inputs must always be encoded. Currently, our ZkpRuntime only offers prove() and verify() methods, but in the future we may offer a jit() method, with all public inputs already encoded. In this scenario, constant inputs would still have to be encoded on every jitted.verify() call.

Furthermore, constant inputs are fundamentally incompatible with any proof system requiring a trusted setup (as in you would have to re-run the trusted setup process).

How to use constant inputs

It's pretty straightforward to use constant inputs, simply use the #[constant] attribute for the relevant arguments you want to be treated as constants.

An example

For example, let's say you've written the following ZKP to evaluate a polynomial on private/secret coefficients:

#![allow(unused)]
fn main() {
use sunscreen::{
    bulletproofs::BulletproofsBackend,
    types::zkp::{BulletproofsField, Field, FieldSpec},
    zkp_program, zkp_var, Compiler, Error, ZkpProgramInput, ZkpRuntime,
};
#[zkp_program]
fn evaluate_polynomial<F: FieldSpec>(
    coefficients: [Field<F>; 100], // private
    #[public] point: Field<F>,
    #[public] expected: Field<F>,
) {
    let mut evaluation = zkp_var!(0);
    let mut power = zkp_var!(1);
    for coeff in coefficients {
        evaluation = evaluation + coeff * power;
        power = power * point;
    }

    evaluation.constrain_eq(expected);
}
}

Tracing

Let's further suppose that your program works correctly but that you are not happy with the performance. The first thing you can do to troubleshoot your ZKP performance is to enable tracing1.

If you run this ZKP program on the polynomial with coefficients 1,...,100 evaluated at the point 2, you'll see a trace like the following:

[TRACE sunscreen_runtime::runtime] Starting JIT (prover)...
[TRACE sunscreen_runtime::runtime] Prover JIT time 0.002542035s
[TRACE sunscreen_runtime::runtime] Starting backend prove...
[TRACE sunscreen_zkp_backend::bulletproofs] Bulletproofs encode time 0.002508913s
[TRACE sunscreen_zkp_backend::bulletproofs] Metrics {
        multipliers: 249,
        constraints: 399,
        phase_one_constraints: 399,
        phase_two_constraints: 0,
    }
[TRACE sunscreen_zkp_backend::bulletproofs] Bulletproofs prover time 0.254062677s
[TRACE sunscreen_runtime::runtime] Starting JIT (verifier)
[TRACE sunscreen_runtime::runtime] Verifier JIT time 0.001074441s
[TRACE sunscreen_runtime::runtime] Starting backend verify...
[TRACE sunscreen_zkp_backend::bulletproofs] Starting backend verify...
[TRACE sunscreen_zkp_backend::bulletproofs] Bulletproofs encode time 0.001557964s
[TRACE sunscreen_zkp_backend::bulletproofs] Bulletproofs verify time 0.022426849s

That's weird; we only have one constrain_eq call, but there are 399 constraints in the proof. What's up with that?

Constant inputs

When we arithmetize your program, there's actually a lot of extra constraints that get added to the low level R1CS representation.

Normally, the number of constraints \(n\) increase linearly with the number of multiplication operations, as multiplication is essentially represented as a dot product where the vectors contain the inputs to all multiplication gates in the constraint system.

Constant inputs are instead placed directly into a weights matrix, and these factors can be applied directly to variables, rather than requiring additional multiplication gates that increase the factor \(n\).

If we change the argument types from #[public] to #[constant] and rerun the trace, we'll see a big improvement:

[TRACE sunscreen_runtime::runtime] Starting JIT (prover)...
[TRACE sunscreen_runtime::runtime] Prover JIT time 0.002529488s
[TRACE sunscreen_runtime::runtime] Starting backend prove...
[TRACE sunscreen_zkp_backend::bulletproofs] Bulletproofs encode time 0.001372845s
[TRACE sunscreen_zkp_backend::bulletproofs] Metrics {
        multipliers: 50,
        constraints: 1,
        phase_one_constraints: 1,
        phase_two_constraints: 0,
    }
[TRACE sunscreen_zkp_backend::bulletproofs] Bulletproofs prover time 0.057199442s
[TRACE sunscreen_runtime::runtime] Starting JIT (verifier)
[TRACE sunscreen_runtime::runtime] Verifier JIT time 0.001072157s
[TRACE sunscreen_runtime::runtime] Starting backend verify...
[TRACE sunscreen_zkp_backend::bulletproofs] Starting backend verify...
[TRACE sunscreen_zkp_backend::bulletproofs] Bulletproofs encode time 0.001272397s
[TRACE sunscreen_zkp_backend::bulletproofs] Bulletproofs verify time 0.007118962s

Because the only multiplications happen with constants, the only constraint in the R1CS circuit is the constrain_eq call. Also notice both the prover and verifier times have gone down dramatically.

1

You can enable tracing by using the env_logger crate, calling env_logger::init() somewhere in your main function, and setting the environment variable RUST_LOG=trace.