From 0637f525737494e1789a458b3c28b50847013900 Mon Sep 17 00:00:00 2001 From: arnaucube Date: Tue, 1 Apr 2025 01:36:37 +0200 Subject: [PATCH] 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. --- src/backends/plonky2/circuits/mainpod.rs | 30 +-- src/backends/plonky2/primitives/merkletree.rs | 1 - .../plonky2/primitives/merkletree_circuit.rs | 20 +- src/backends/plonky2/primitives/mod.rs | 1 + src/backends/plonky2/primitives/signature.rs | 58 +++- .../plonky2/primitives/signature_circuit.rs | 251 ++++++++++++++++++ src/middleware/mod.rs | 6 +- 7 files changed, 325 insertions(+), 42 deletions(-) create mode 100644 src/backends/plonky2/primitives/signature_circuit.rs diff --git a/src/backends/plonky2/circuits/mainpod.rs b/src/backends/plonky2/circuits/mainpod.rs index c0cdfe9..b9dae14 100644 --- a/src/backends/plonky2/circuits/mainpod.rs +++ b/src/backends/plonky2/circuits/mainpod.rs @@ -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) -> Result { // 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, 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, @@ -359,17 +359,17 @@ impl OperationVerifyTarget { } } -struct MainPodVerifyGate { +struct MainPodVerifyGadget { params: Params, } -impl MainPodVerifyGate { +impl MainPodVerifyGadget { fn eval(&self, builder: &mut CircuitBuilder) -> Result { 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) -> Result { - 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::::new(config); - let signed_pod_verify = SignedPodVerifyGate { params }.eval(&mut builder)?; + let signed_pod_verify = SignedPodVerifyGadget { params }.eval(&mut builder)?; let mut pw = PartialWitness::::new(); let kvs = [ @@ -543,7 +543,7 @@ mod tests { .map(|_| builder.add_virtual_statement(¶ms)) .collect(); - let operation_verify = OperationVerifyGate { + let operation_verify = OperationVerifyGadget { params: params.clone(), } .eval( diff --git a/src/backends/plonky2/primitives/merkletree.rs b/src/backends/plonky2/primitives/merkletree.rs index c98729a..999cee1 100644 --- a/src/backends/plonky2/primitives/merkletree.rs +++ b/src/backends/plonky2/primitives/merkletree.rs @@ -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 diff --git a/src/backends/plonky2/primitives/merkletree_circuit.rs b/src/backends/plonky2/primitives/merkletree_circuit.rs index fe4d236..27a0d1c 100644 --- a/src/backends/plonky2/primitives/merkletree_circuit.rs +++ b/src/backends/plonky2/primitives/merkletree_circuit.rs @@ -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) -> Result { // 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, } -impl MerkleProofExistenceGate { +impl MerkleProofExistenceGadget { /// creates the targets and defines the logic of the circuit pub fn eval(&self, builder: &mut CircuitBuilder) -> Result { // create the targets @@ -486,7 +486,7 @@ pub mod tests { let mut builder = CircuitBuilder::::new(config); let mut pw = PartialWitness::::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::::new(config); let mut pw = PartialWitness::::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::::new(config); let mut pw = PartialWitness::::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::::new(config); let mut pw = PartialWitness::::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 diff --git a/src/backends/plonky2/primitives/mod.rs b/src/backends/plonky2/primitives/mod.rs index bddbc4d..1fa75d7 100644 --- a/src/backends/plonky2/primitives/mod.rs +++ b/src/backends/plonky2/primitives/mod.rs @@ -1,3 +1,4 @@ pub mod merkletree; mod merkletree_circuit; pub mod signature; +mod signature_circuit; diff --git a/src/backends/plonky2/primitives/signature.rs b/src/backends/plonky2/primitives/signature.rs index 3c4487f..a4ced69 100644 --- a/src/backends/plonky2/primitives/signature.rs +++ b/src/backends/plonky2/primitives/signature.rs @@ -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 = dummy_public_inputs().unwrap(); } pub struct ProverParams { prover: ProverCircuitData, - circuit: SignatureCircuit, + circuit: SignatureInternalCircuit, } #[derive(Clone, Debug)] -pub struct VerifierParams(VerifierCircuitData); +pub struct VerifierParams(pub(crate) VerifierCircuitData); #[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, SignatureCircuit)> { + fn builder() -> Result<(CircuitBuilder, SignatureInternalCircuit)> { // notice that we use the 'zk' config let config = CircuitConfig::standard_recursion_zk_config(); let mut builder = CircuitBuilder::::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> { + 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 { + 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, pk_targ: HashOutTarget, msg_targ: Vec, s_targ: HashOutTarget, } -impl SignatureCircuit { +impl SignatureInternalCircuit { /// creates the targets and defines the logic of the circuit fn add_targets(builder: &mut CircuitBuilder) -> Result { // 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(()) + } } diff --git a/src/backends/plonky2/primitives/signature_circuit.rs b/src/backends/plonky2/primitives/signature_circuit.rs new file mode 100644 index 0000000..802b4e4 --- /dev/null +++ b/src/backends/plonky2/primitives/signature_circuit.rs @@ -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 = 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, +} + +impl SignatureVerifyGadget { + pub fn verifier_data() -> Result> { + // notice that we use the 'zk' config + let config = CircuitConfig::standard_recursion_zk_config(); + let mut builder = CircuitBuilder::::new(config); + let circuit = SignatureVerifyGadget {}.eval(&mut builder)?; + + let circuit_data = builder.build::(); + Ok(circuit_data.verifier_data()) + } +} + +impl SignatureVerifyGadget { + /// creates the targets and defines the logic of the circuit + pub fn eval(&self, builder: &mut CircuitBuilder) -> Result { + 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 = [pk_targ.elements.to_vec(), msg_targ.elements.to_vec()].concat(); + let s_targ = builder.hash_n_to_hash_no_pad::(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::(&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, + 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 = [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::::new(config); + let mut pw = PartialWitness::::new(); + + let targets = SignatureVerifyGadget {}.eval(&mut builder)?; + targets.set_targets(&mut pw, true, pk, msg, sig)?; + + // generate & verify proof + let data = builder.build::(); + 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::::new(config); + let mut pw = PartialWitness::::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::(); + 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::::new(config); + let mut pw = PartialWitness::::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::(); + 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(()) + } +} diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 6074039..59189ad 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -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, } } }