RustFLPCP lets a client prove its input is valid, such as a one-hot vote or a vector with a
bounded norm, to a verifier that only computes linear functions of the input and the proof. The
check therefore works on data the verifier cannot read: inner-product functional encryption
(IPFE) ciphertexts or secret shares. It implements the fully linear PCPs of
Boneh et al. (CRYPTO 2019) as a generic engine
and is the proof layer of PEAR (PoPETs 2025):
write a validity circuit once and get a prover, explicit linear queries, and a decision
procedure over any arkworks FFT field. For a 1,024-entry one-hot vector,
proving takes about 1.5 ms, building a query about 0.6 ms, and checking the proof against it
about 0.12 ms on one CPU core (cargo bench --bench flp).
use ark_bls12_381::Fr;
use rand::rngs::OsRng;
use rust_flpcp::{Flp, OneHot};
let rng = &mut OsRng;
let flp = Flp::<Fr, _>::new(OneHot { len: 4 })?;
let vector = flp.prove(&[0u64, 0, 1, 0].map(Fr::from), rng)?;
let query = flp.query(rng);
let answers: Vec<Fr> = query
.linear_queries()
.iter()
.map(|linear| {
let inner: Fr = linear.coeffs.iter().zip(&vector).map(|(c, v)| *c * v).sum();
inner + linear.constant
})
.collect();
assert!(flp.decide(&query, &answers));Under IPFE each coeffs is a function key and the verifier sees only the answers;
flp.verify(&query, &vector) is the plaintext shortcut. This is examples/quickstart.rs.
| Circuit | Accepts |
|---|---|
Range { len, bounds } |
every value in bounds (Range::binary(len) for bits) |
BitRange { len, bounds } |
every value in bounds, via bit decompositions (for wide ranges) |
OneHot { len } |
exactly one value is 1, the rest 0 |
Membership { len, values } |
every value is one of values |
NormBound { len, input_bits, norm_bits, square_chunk } |
values below 2^input_bits, sum of squares below 2^norm_bits |
SignedNormBound { .. } |
the same for signed values, as in federated-learning clipping |
MeanVariance { len, input_bits } |
values below 2^input_bits, each claimed y equals x^2, for private mean and variance |
Circuits with auxiliary bits build their input with encode. Run cargo run --example costs
for each circuit's vector length and query count.
Implement Circuit<F>: input_len, gadgets, and an eval generic over W: Wire<F> that
returns outputs which must all be zero for a valid input. Wires only support affine operations;
every multiplication goes through calls.call(GadgetId(i), inputs) on a gadget (RangeProduct,
SetProduct, Square, ParallelSum, or your own Gadget<F>). The same eval builds both the
proof and the queries. Flp::new runs each gadget's check_params, so a RangeProduct or
SetProduct constant too large for the field is rejected. ParallelSum proves only a sum, so
never use it to batch zero checks.
- Pass a cryptographically secure generator (
rand0.8, such asOsRng);test_rng()is for tests only. - The
Queryholds the verifier's secret randomness: keep it from the prover. Never answer one vector under two different queries, which reveals a linear function of the input. Reusing one query across many provers, as PEAR does, is covered by the reusable soundness and zero-knowledge proofs in the PEAR paper. - A cheating proof passes with probability about its polynomial degree over the field size.
[dependencies]
rust_flpcp = { git = "https://github.com/WeiqiNs/RustFLPCP" }
ark-bls12-381 = { version = "0.6", default-features = false, features = ["scalar_field"] }
rand = "0.8"Any arkworks FFT field works in place of BLS12-381's scalar field. rand stays on 0.8 because
arkworks 0.6 takes rand 0.8 generators. The parallel feature enables arkworks' multithreaded
FFTs.
cargo test --all
cargo clippy --all --all-targets -- -D warnings
cargo llvm-cov --all --fail-uncovered-lines 0
cargo bench --bench flp