add circuit to verify signatures (SignatureVerifyGadget) (#167)

* implement circuit to verify signature (proof-based signature), ie. a 1-level recursion verification

* as agreed in the call, rename Gate -> Gadget when it's not a 'gate'

* make SignatureVerifyGadget conditional on the selector input

* small naming polish

* sigverifygadget: add s computation in-circuit, connect pk,msg,s to internalproof's public_inputs

* optimize signature verify

---------

Co-authored-by: Eduard S. <eduardsanou@posteo.net>
This commit is contained in:
arnaucube 2025-04-01 01:36:37 +02:00 committed by GitHub
parent d00ff95f41
commit 0637f52573
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 325 additions and 42 deletions

View file

@ -21,7 +21,7 @@ use crate::backends::plonky2::circuits::common::{
};
use crate::backends::plonky2::primitives::merkletree::MerkleTree;
use crate::backends::plonky2::primitives::merkletree::{
MerkleProofExistenceGate, MerkleProofExistenceTarget,
MerkleProofExistenceGadget, MerkleProofExistenceTarget,
};
use crate::middleware::{
hash_str, AnchoredKey, NativeOperation, NativePredicate, Params, PodType, Statement,
@ -34,18 +34,18 @@ use super::common::Flattenable;
// SignedPod verification
//
struct SignedPodVerifyGate {
struct SignedPodVerifyGadget {
params: Params,
}
impl SignedPodVerifyGate {
impl SignedPodVerifyGadget {
fn eval(&self, builder: &mut CircuitBuilder<F, D>) -> Result<SignedPodVerifyTarget> {
// 2. Verify id
let id = builder.add_virtual_hash();
let mut mt_proofs = Vec::new();
for _ in 0..self.params.max_signed_pod_values {
let mt_proof = MerkleProofExistenceGate {
max_depth: self.params.max_depth_mt_gate,
let mt_proof = MerkleProofExistenceGadget {
max_depth: self.params.max_depth_mt_gadget,
}
.eval(builder)?;
builder.connect_hashes(id, mt_proof.root);
@ -100,7 +100,7 @@ impl SignedPodVerifyTarget {
fn set_targets(&self, pw: &mut PartialWitness<F>, input: &SignedPodVerifyInput) -> Result<()> {
assert!(input.kvs.len() <= self.params.max_signed_pod_values);
let tree = MerkleTree::new(self.params.max_depth_mt_gate, &input.kvs)?;
let tree = MerkleTree::new(self.params.max_depth_mt_gadget, &input.kvs)?;
// First handle the type entry, then the rest of the entries, and finally pad with
// repetitions of the type entry (which always exists)
@ -125,11 +125,11 @@ impl SignedPodVerifyTarget {
// MainPod verification
//
struct OperationVerifyGate {
struct OperationVerifyGadget {
params: Params,
}
impl OperationVerifyGate {
impl OperationVerifyGadget {
fn eval(
&self,
builder: &mut CircuitBuilder<F, D>,
@ -359,17 +359,17 @@ impl OperationVerifyTarget {
}
}
struct MainPodVerifyGate {
struct MainPodVerifyGadget {
params: Params,
}
impl MainPodVerifyGate {
impl MainPodVerifyGadget {
fn eval(&self, builder: &mut CircuitBuilder<F, D>) -> Result<MainPodVerifyTarget> {
let params = &self.params;
// 1. Verify all input signed pods
let mut signed_pods = Vec::new();
for _ in 0..params.max_input_signed_pods {
let signed_pod = SignedPodVerifyGate {
let signed_pod = SignedPodVerifyGadget {
params: params.clone(),
}
.eval(builder)?;
@ -421,7 +421,7 @@ impl MainPodVerifyGate {
let mut op_verifications = Vec::new();
for (i, (st, op)) in input_statements.iter().zip(operations.iter()).enumerate() {
let prev_statements = &statements[..input_statements_offset + i - 1];
let op_verification = OperationVerifyGate {
let op_verification = OperationVerifyGadget {
params: params.clone(),
}
.eval(builder, st, op, prev_statements)?;
@ -478,7 +478,7 @@ pub struct MainPodVerifyCircuit {
impl MainPodVerifyCircuit {
pub fn eval(&self, builder: &mut CircuitBuilder<F, D>) -> Result<MainPodVerifyTarget> {
let main_pod = MainPodVerifyGate {
let main_pod = MainPodVerifyGadget {
params: self.params.clone(),
}
.eval(builder)?;
@ -505,7 +505,7 @@ mod tests {
let config = CircuitConfig::standard_recursion_config();
let mut builder = CircuitBuilder::<F, D>::new(config);
let signed_pod_verify = SignedPodVerifyGate { params }.eval(&mut builder)?;
let signed_pod_verify = SignedPodVerifyGadget { params }.eval(&mut builder)?;
let mut pw = PartialWitness::<F>::new();
let kvs = [
@ -543,7 +543,7 @@ mod tests {
.map(|_| builder.add_virtual_statement(&params))
.collect();
let operation_verify = OperationVerifyGate {
let operation_verify = OperationVerifyGadget {
params: params.clone(),
}
.eval(

View file

@ -10,7 +10,6 @@ use std::iter::IntoIterator;
use crate::backends::counter;
use crate::backends::plonky2::basetypes::{hash_fields, Hash, Value, EMPTY_HASH, F};
// mod merkletree_circuit;
pub use super::merkletree_circuit::*;
/// Implements the MerkleTree specified at

View file

@ -27,11 +27,11 @@ use crate::backends::plonky2::basetypes::{Hash, Value, D, EMPTY_HASH, EMPTY_VALU
use crate::backends::plonky2::circuits::common::{CircuitBuilderPod, ValueTarget};
use crate::backends::plonky2::primitives::merkletree::MerkleProof;
/// `MerkleProofGate` allows to verify both proofs of existence and proofs
/// `MerkleProofGadget` allows to verify both proofs of existence and proofs
/// non-existence with the same circuit.
/// If only proofs of existence are needed, use `MerkleProofExistenceGate`,
/// If only proofs of existence are needed, use `MerkleProofExistenceGadget`,
/// which requires less amount of constraints.
pub struct MerkleProofGate {
pub struct MerkleProofGadget {
pub max_depth: usize,
}
@ -47,7 +47,7 @@ pub struct MerkleProofTarget {
pub other_value: ValueTarget,
}
impl MerkleProofGate {
impl MerkleProofGadget {
/// creates the targets and defines the logic of the circuit
pub fn eval(&self, builder: &mut CircuitBuilder<F, D>) -> Result<MerkleProofTarget> {
// create the targets
@ -179,7 +179,7 @@ impl MerkleProofTarget {
/// `MerkleProofExistenceCircuit` allows to verify proofs of existence only. If
/// proofs of non-existence are needed, use `MerkleProofCircuit`.
pub struct MerkleProofExistenceGate {
pub struct MerkleProofExistenceGadget {
pub max_depth: usize,
}
@ -191,7 +191,7 @@ pub struct MerkleProofExistenceTarget {
pub siblings: Vec<HashOutTarget>,
}
impl MerkleProofExistenceGate {
impl MerkleProofExistenceGadget {
/// creates the targets and defines the logic of the circuit
pub fn eval(&self, builder: &mut CircuitBuilder<F, D>) -> Result<MerkleProofExistenceTarget> {
// create the targets
@ -486,7 +486,7 @@ pub mod tests {
let mut builder = CircuitBuilder::<F, D>::new(config);
let mut pw = PartialWitness::<F>::new();
let targets = MerkleProofGate { max_depth }.eval(&mut builder)?;
let targets = MerkleProofGadget { max_depth }.eval(&mut builder)?;
targets.set_targets(&mut pw, existence, tree.root(), proof, key, value)?;
// generate & verify proof
@ -525,7 +525,7 @@ pub mod tests {
let mut builder = CircuitBuilder::<F, D>::new(config);
let mut pw = PartialWitness::<F>::new();
let targets = MerkleProofExistenceGate { max_depth }.eval(&mut builder)?;
let targets = MerkleProofExistenceGadget { max_depth }.eval(&mut builder)?;
targets.set_targets(&mut pw, tree.root(), proof, key, value)?;
// generate & verify proof
@ -592,7 +592,7 @@ pub mod tests {
let mut builder = CircuitBuilder::<F, D>::new(config);
let mut pw = PartialWitness::<F>::new();
let targets = MerkleProofGate { max_depth }.eval(&mut builder)?;
let targets = MerkleProofGadget { max_depth }.eval(&mut builder)?;
targets.set_targets(&mut pw, proof.existence, tree.root(), proof, key, value)?;
// generate & verify proof
@ -633,7 +633,7 @@ pub mod tests {
let mut builder = CircuitBuilder::<F, D>::new(config);
let mut pw = PartialWitness::<F>::new();
let targets = MerkleProofGate { max_depth }.eval(&mut builder)?;
let targets = MerkleProofGadget { max_depth }.eval(&mut builder)?;
targets.set_targets(&mut pw, true, tree2.root(), proof, key, value)?;
// generate proof, expecting it to fail (since we're using the wrong

View file

@ -1,3 +1,4 @@
pub mod merkletree;
mod merkletree_circuit;
pub mod signature;
mod signature_circuit;

View file

@ -1,6 +1,7 @@
//! Proof-based signatures using Plonky2 proofs, following
//! https://eprint.iacr.org/2024/1553 .
use anyhow::Result;
use lazy_static::lazy_static;
use plonky2::{
field::types::Sample,
hash::{
@ -21,23 +22,31 @@ use plonky2::{
use crate::backends::plonky2::basetypes::{Proof, Value, C, D, F, VALUE_SIZE};
use lazy_static::lazy_static;
pub use super::signature_circuit::*;
lazy_static! {
static ref PP: ProverParams = Signature::prover_params().unwrap();
static ref VP: VerifierParams = Signature::verifier_params().unwrap();
/// Signature prover parameters
pub static ref PP: ProverParams = Signature::prover_params().unwrap();
/// Signature verifier parameters
pub static ref VP: VerifierParams = Signature::verifier_params().unwrap();
/// DUMMY_SIGNATURE is used for conditionals where we want to use a `selector` to enable or
/// disable signature verification.
pub static ref DUMMY_SIGNATURE: Signature = dummy_signature().unwrap();
/// DUMMY_PUBLIC_INPUTS accompanies the DUMMY_SIGNATURE.
pub static ref DUMMY_PUBLIC_INPUTS: Vec<F> = dummy_public_inputs().unwrap();
}
pub struct ProverParams {
prover: ProverCircuitData<F, C, D>,
circuit: SignatureCircuit,
circuit: SignatureInternalCircuit,
}
#[derive(Clone, Debug)]
pub struct VerifierParams(VerifierCircuitData<F, C, D>);
pub struct VerifierParams(pub(crate) VerifierCircuitData<F, C, D>);
#[derive(Clone, Debug)]
pub struct SecretKey(Value);
pub struct SecretKey(pub(crate) Value);
#[derive(Clone, Debug)]
pub struct PublicKey(pub(crate) Value);
@ -90,12 +99,12 @@ impl Signature {
Ok((pp, vp))
}
fn builder() -> Result<(CircuitBuilder<F, D>, SignatureCircuit)> {
fn builder() -> Result<(CircuitBuilder<F, D>, SignatureInternalCircuit)> {
// notice that we use the 'zk' config
let config = CircuitConfig::standard_recursion_zk_config();
let mut builder = CircuitBuilder::<F, D>::new(config);
let circuit = SignatureCircuit::add_targets(&mut builder)?;
let circuit = SignatureInternalCircuit::add_targets(&mut builder)?;
Ok((builder, circuit))
}
@ -113,21 +122,35 @@ impl Signature {
}
}
/// The SignatureCircuit implements the circuit used for the proof of the
/// argument described at https://eprint.iacr.org/2024/1553.
fn dummy_public_inputs() -> Result<Vec<F>> {
let sk = SecretKey(Value::from(0));
let pk = sk.public_key();
let msg = Value::from(0);
let s = Value(PoseidonHash::hash_no_pad(&[pk.0 .0, msg.0].concat()).elements);
Ok([pk.0 .0, msg.0, s.0].concat())
}
fn dummy_signature() -> Result<Signature> {
let sk = SecretKey(Value::from(0));
let msg = Value::from(0);
sk.sign(msg)
}
/// The SignatureInternalCircuit implements the circuit used for the proof of
/// the argument described at https://eprint.iacr.org/2024/1553.
///
/// The circuit proves that for the given public inputs (pk, msg, s), the Prover
/// knows the secret (sk) such that:
/// i) pk == H(sk)
/// ii) s == H(pk, msg)
struct SignatureCircuit {
struct SignatureInternalCircuit {
sk_targ: Vec<Target>,
pk_targ: HashOutTarget,
msg_targ: Vec<Target>,
s_targ: HashOutTarget,
}
impl SignatureCircuit {
impl SignatureInternalCircuit {
/// creates the targets and defines the logic of the circuit
fn add_targets(builder: &mut CircuitBuilder<F, D>) -> Result<Self> {
// create the targets
@ -182,7 +205,6 @@ pub mod tests {
use super::*;
// Note: this test must be run with the `--release` flag.
#[test]
fn test_signature() -> Result<()> {
let sk = SecretKey::new();
@ -203,4 +225,14 @@ pub mod tests {
Ok(())
}
#[test]
fn test_dummy_signature() -> Result<()> {
let sk = SecretKey(Value::from(0));
let pk = sk.public_key();
let msg = Value::from(0);
DUMMY_SIGNATURE.clone().verify(&pk, msg)?;
Ok(())
}
}

View file

@ -0,0 +1,251 @@
#![allow(unused)]
use anyhow::Result;
use lazy_static::lazy_static;
use plonky2::{
field::types::Field,
hash::{
hash_types::{HashOut, HashOutTarget},
poseidon::PoseidonHash,
},
iop::{
target::{BoolTarget, Target},
witness::{PartialWitness, WitnessWrite},
},
plonk::circuit_builder::CircuitBuilder,
plonk::circuit_data::{
CircuitConfig, CircuitData, ProverCircuitData, VerifierCircuitData, VerifierCircuitTarget,
},
plonk::config::Hasher,
plonk::proof::{ProofWithPublicInputs, ProofWithPublicInputsTarget},
};
use super::signature::{PublicKey, SecretKey, Signature, DUMMY_PUBLIC_INPUTS, DUMMY_SIGNATURE};
use crate::backends::plonky2::basetypes::{
Hash, Proof, Value, C, D, EMPTY_HASH, EMPTY_VALUE, F, VALUE_SIZE,
};
use crate::backends::plonky2::circuits::common::{CircuitBuilderPod, ValueTarget};
lazy_static! {
/// SignatureVerifyGadget VerifierCircuitData
pub static ref S_VD: VerifierCircuitData<F,C,D> = SignatureVerifyGadget::verifier_data().unwrap();
}
pub struct SignatureVerifyGadget {}
pub struct SignatureVerifyTarget {
// verifier_data of the SignatureInternalCircuit
verifier_data_targ: VerifierCircuitTarget,
selector: BoolTarget,
pk: ValueTarget,
msg: ValueTarget,
// proof of the SignatureInternalCircuit (=signature::Signature.0)
proof: ProofWithPublicInputsTarget<D>,
}
impl SignatureVerifyGadget {
pub fn verifier_data() -> Result<VerifierCircuitData<F, C, D>> {
// notice that we use the 'zk' config
let config = CircuitConfig::standard_recursion_zk_config();
let mut builder = CircuitBuilder::<F, D>::new(config);
let circuit = SignatureVerifyGadget {}.eval(&mut builder)?;
let circuit_data = builder.build::<C>();
Ok(circuit_data.verifier_data())
}
}
impl SignatureVerifyGadget {
/// creates the targets and defines the logic of the circuit
pub fn eval(&self, builder: &mut CircuitBuilder<F, D>) -> Result<SignatureVerifyTarget> {
let selector = builder.add_virtual_bool_target_safe();
let common_data = super::signature::VP.0.common.clone();
// targets related to the 'public inputs' for the verification of the
// `SignatureInternalCircuit` proof.
let pk_targ = builder.add_virtual_value();
let msg_targ = builder.add_virtual_value();
let inp: Vec<Target> = [pk_targ.elements.to_vec(), msg_targ.elements.to_vec()].concat();
let s_targ = builder.hash_n_to_hash_no_pad::<PoseidonHash>(inp);
let verifier_data_targ =
builder.add_virtual_verifier_data(common_data.config.fri_config.cap_height);
let proof_targ = builder.add_virtual_proof_with_pis(&common_data);
let dummy_pi = DUMMY_PUBLIC_INPUTS.clone();
let pk_targ_dummy =
builder.constant_value(Value(dummy_pi[..VALUE_SIZE].try_into().unwrap()));
let msg_targ_dummy = builder.constant_value(Value(
dummy_pi[VALUE_SIZE..VALUE_SIZE * 2].try_into().unwrap(),
));
let s_targ_dummy =
builder.constant_value(Value(dummy_pi[VALUE_SIZE * 2..].try_into().unwrap()));
// connect the {pk, msg, s} with the proof_targ.public_inputs conditionally
let pk_targ_connect = builder.select_value(selector, pk_targ, pk_targ_dummy);
let msg_targ_connect = builder.select_value(selector, msg_targ, msg_targ_dummy);
let s_targ_connect = builder.select_value(
selector,
ValueTarget {
elements: s_targ.elements,
},
s_targ_dummy,
);
for i in 0..VALUE_SIZE {
builder.connect(pk_targ_connect.elements[i], proof_targ.public_inputs[i]);
builder.connect(
msg_targ_connect.elements[i],
proof_targ.public_inputs[VALUE_SIZE + i],
);
builder.connect(
s_targ_connect.elements[i],
proof_targ.public_inputs[(2 * VALUE_SIZE) + i],
);
}
builder.verify_proof::<C>(&proof_targ, &verifier_data_targ, &common_data);
Ok(SignatureVerifyTarget {
verifier_data_targ,
selector,
pk: pk_targ,
msg: msg_targ,
proof: proof_targ,
})
}
}
impl SignatureVerifyTarget {
/// assigns the given values to the targets
pub fn set_targets(
&self,
pw: &mut PartialWitness<F>,
selector: bool,
pk: PublicKey,
msg: Value,
signature: Signature,
) -> Result<()> {
pw.set_bool_target(self.selector, selector)?;
pw.set_target_arr(&self.pk.elements, &pk.0 .0)?;
pw.set_target_arr(&self.msg.elements, &msg.0)?;
// note that this hash is checked again in-circuit at the `SignatureInternalCircuit`
let s = Value(PoseidonHash::hash_no_pad(&[pk.0 .0, msg.0].concat()).elements);
let public_inputs: Vec<F> = [pk.0 .0, msg.0, s.0].concat();
if selector {
pw.set_proof_with_pis_target(
&self.proof,
&ProofWithPublicInputs {
proof: signature.0,
public_inputs,
},
)?;
} else {
pw.set_proof_with_pis_target(
&self.proof,
&ProofWithPublicInputs {
proof: DUMMY_SIGNATURE.0.clone(),
public_inputs: DUMMY_PUBLIC_INPUTS.clone(),
},
)?;
}
pw.set_verifier_data_target(
&self.verifier_data_targ,
&super::signature::VP.0.verifier_only,
)?;
Ok(())
}
}
#[cfg(test)]
pub mod tests {
use crate::backends::plonky2::basetypes::Hash;
use crate::backends::plonky2::primitives::signature::SecretKey;
use super::*;
#[test]
fn test_signature_gadget() -> Result<()> {
// generate a valid signature
let sk = SecretKey::new();
let pk = sk.public_key();
let msg = Value::from(42);
let sig = sk.sign(msg)?;
sig.verify(&pk, msg)?;
// circuit
let config = CircuitConfig::standard_recursion_zk_config();
let mut builder = CircuitBuilder::<F, D>::new(config);
let mut pw = PartialWitness::<F>::new();
let targets = SignatureVerifyGadget {}.eval(&mut builder)?;
targets.set_targets(&mut pw, true, pk, msg, sig)?;
// generate & verify proof
let data = builder.build::<C>();
let proof = data.prove(pw)?;
data.verify(proof.clone())?;
// verify the proof with the lazy_static loaded verifier_data (S_VD)
S_VD.verify(ProofWithPublicInputs {
proof: proof.proof.clone(),
public_inputs: vec![],
})?;
Ok(())
}
#[test]
fn test_signature_gadget_disabled() -> Result<()> {
// generate a valid signature
let sk = SecretKey::new();
let pk = sk.public_key();
let msg = Value::from(42);
let sig = sk.sign(msg)?;
// verification should pass
sig.verify(&pk, msg)?;
// replace the message, so that verifications should fail
let msg = Value::from(24);
// expect signature native verification to fail
let v = sig.verify(&pk, Value::from(24));
assert!(v.is_err(), "should fail to verify");
// circuit
let config = CircuitConfig::standard_recursion_zk_config();
let mut builder = CircuitBuilder::<F, D>::new(config);
let mut pw = PartialWitness::<F>::new();
let targets = SignatureVerifyGadget {}.eval(&mut builder)?;
targets.set_targets(&mut pw, true, pk.clone(), msg, sig.clone())?; // selector=true
// generate proof, and expect it to fail
let data = builder.build::<C>();
assert!(data.prove(pw).is_err()); // expect prove to fail
// build the circuit again, but now disable the selector that disables
// the in-circuit signature verification
let config = CircuitConfig::standard_recursion_zk_config();
let mut builder = CircuitBuilder::<F, D>::new(config);
let mut pw = PartialWitness::<F>::new();
let targets = SignatureVerifyGadget {}.eval(&mut builder)?;
targets.set_targets(&mut pw, false, pk, msg, sig)?; // selector=false
// generate & verify proof
let data = builder.build::<C>();
let proof = data.prove(pw)?;
data.verify(proof.clone())?;
// verify the proof with the lazy_static loaded verifier_data (S_VD)
S_VD.verify(ProofWithPublicInputs {
proof: proof.proof.clone(),
public_inputs: vec![],
})?;
Ok(())
}
}

View file

@ -105,8 +105,8 @@ pub struct Params {
// in a custom predicate
pub max_custom_predicate_arity: usize,
pub max_custom_batch_size: usize,
// maximum depth for merkle tree gates
pub max_depth_mt_gate: usize,
// maximum depth for merkle tree gadget
pub max_depth_mt_gadget: usize,
}
impl Default for Params {
@ -121,7 +121,7 @@ impl Default for Params {
max_operation_args: 5,
max_custom_predicate_arity: 5,
max_custom_batch_size: 5,
max_depth_mt_gate: 32,
max_depth_mt_gadget: 32,
}
}
}