arnaucube
arnaucube/plonky2-crypto-gadgetsPublic Self-hosted repository
add ElGamal gadget
Ahmad root@ahmadafuni.com
Tue, 17 Feb 2026 19:53:29 +1000
commit

49e4c0694d99b8cc27329387955f0570e4196ae3

parent

59f79cb3ac800b2f8469ae6da9a0a4a7d9ab0c44

M Cargo.toml → Cargo.toml

@@ -1,8 +1,12 @@

[workspace] members = [ - "aes-gcm" + "aes-gcm", + "ecgfp5" + ] resolver = "2" [workspace.dependencies] +plonky2 = { git = "https://github.com/0xPARC/plonky2.git", rev = "109d517d09c210ae4c2cee381d3e3fbc04aa3812" } +anyhow = "1.0.56"
M aes-gcm/Cargo.toml → aes-gcm/Cargo.toml

@@ -4,8 +4,8 @@ version = "0.1.0"

edition = "2024" [dependencies] -plonky2 = { git = "https://github.com/0xPARC/plonky2.git", rev = "109d517d09c210ae4c2cee381d3e3fbc04aa3812" } -anyhow = "1.0.56" +plonky2 = { workspace = true } +anyhow = { workspace = true } [dev-dependencies] rand = "0.10.0"
A ecgfp5/Cargo.toml

@@ -0,0 +1,12 @@

+[package] +name = "plonky2-ecgfp5" +version = "0.1.0" +edition = "2024" + +[dependencies] +plonky2 = { workspace = true } +anyhow = { workspace = true } +num = { version = "0.4.3", features = ["num-bigint"] } +pod2 = { git = "https://github.com/0xPARC/pod2", rev = "cdf227e3530247e07e375daed007cae7d443b4ae" } +num-bigint = { version = "0.4.6", features = ["rand"] } +rand = "0.8.5"
A ecgfp5/src/elgamal.rs

@@ -0,0 +1,134 @@

+use std::array; + +use num::bigint::BigUint; +use num_bigint::RandBigInt; +use plonky2::field::{ + extension::quintic::QuinticExtension, + goldilocks_field::GoldilocksField as F, + types::{Field, Field64}, +}; +use pod2::backends::plonky2::primitives::ec::curve::{GROUP_ORDER, Point}; +use rand::{Rng, rngs::OsRng}; + +pub mod circuit; + +// Message encoding constants +pub const BITS_PER_LIMB: usize = 32; +pub const TOTAL_BITS: usize = 5 * BITS_PER_LIMB; + +pub struct ECGFP5SecretKey(pub BigUint); + +impl ECGFP5SecretKey { + pub fn new(s: BigUint) -> Self { + assert!(&s < &GROUP_ORDER); + Self(s) + } + + pub fn rand() -> Self { + Self(OsRng.gen_biguint_below(&GROUP_ORDER)) + } + + pub fn public_key(&self) -> Point { + &self.0 * Point::generator() + } +} + +pub fn elgamal_encrypt(pk: Point, nonce: &BigUint, msg: Point) -> (Point, Point) { + assert!(nonce < &GROUP_ORDER); + let nonce_times_gen = nonce * Point::generator(); + let nonce_times_pk = nonce * pk; + (nonce_times_gen, msg + nonce_times_pk) +} + +pub fn elgamal_decrypt(sk: ECGFP5SecretKey, ct: (Point, Point)) -> Point { + let nonce_times_pk = &sk.0 * ct.0; + ct.1 + nonce_times_pk.inverse() +} + +/// Convert `TOTAL_BITS`-bit biguint to a point by padding with random bits. +pub fn biguint_to_point(x: &BigUint) -> Point { + let num_bits = x.bits() as usize; + assert!(num_bits <= TOTAL_BITS); + + let bs_bits = [x.to_radix_le(2), vec![0; TOTAL_BITS - num_bits]].concat(); + let bs_fields: Vec<F> = bs_bits + .chunks(BITS_PER_LIMB) + .map(|bits| { + F::from_noncanonical_biguint( + BigUint::from_radix_le(bits, 2).expect("Should be a valid bit decomposition"), + ) + }) + .collect(); + + std::iter::repeat_with(|| { + let ec_fields: [_; 5] = array::from_fn(|i| { + let random_element: u64 = OsRng.gen_range(0..F::ORDER - bs_fields[i].0); + F::from_canonical_u64( + bs_fields[i].0 + ((random_element >> BITS_PER_LIMB) << BITS_PER_LIMB), + ) + }); + QuinticExtension(ec_fields) + }) + .find_map(|w| Point::decompress_into_subgroup(&w).ok()) + .expect("Should be a match!") +} + +pub fn point_to_biguint(p: Point) -> BigUint { + let msg_bits: Vec<_> = p + .compress_from_subgroup() + .unwrap() + .0 + .into_iter() + .flat_map(|f| { + BigUint::from(f.0) + .to_radix_le(2) + .into_iter() + .take(BITS_PER_LIMB) + .collect::<Vec<_>>() + }) + .collect(); + BigUint::from_bytes_le( + &BigUint::from_radix_le(&msg_bits, 2) + .expect("Should be a valid bit slice.") + .to_radix_le(256), + ) +} + +#[cfg(test)] +mod tests { + use num::BigUint; + use num_bigint::RandBigInt; + use pod2::backends::plonky2::primitives::ec::curve::{GROUP_ORDER, Point}; + use rand::rngs::OsRng; + + use crate::elgamal::{ + ECGFP5SecretKey, TOTAL_BITS, biguint_to_point, elgamal_decrypt, elgamal_encrypt, + point_to_biguint, + }; + + #[test] + fn msg_encoding_roundtrip() { + (0..100).for_each(|_| { + let test_val = OsRng.gen_biguint_below(&BigUint::from(2u32).pow(TOTAL_BITS as u32)); + let pt = biguint_to_point(&test_val); + let maybe_test_val = point_to_biguint(pt); + assert_eq!(test_val, maybe_test_val); + }) + } + + #[test] + fn round_trip_encryption() { + (0..100).for_each(|_| { + let sk = ECGFP5SecretKey::rand(); + let pk = sk.public_key(); + + let msg = Point::new_rand_from_subgroup(); + let nonce = OsRng.gen_biguint_below(&GROUP_ORDER); + + let encrypted_msg = elgamal_encrypt(pk, &nonce, msg); + let decrypted_msg = elgamal_decrypt(sk, encrypted_msg); + + assert_eq!(msg, decrypted_msg); + }) + } +}
A ecgfp5/src/elgamal/circuit.rs

@@ -0,0 +1,163 @@

+use plonky2::{ + field::extension::Extendable, hash::hash_types::RichField, iop::witness::PartialWitness, + plonk::circuit_builder::CircuitBuilder, +}; +use pod2::backends::plonky2::{ + basetypes::{D, F}, + primitives::ec::{ + bits::{BigUInt320Target, CircuitBuilderBits}, + curve::{CircuitBuilderElliptic, Point, PointTarget, WitnessWriteCurve}, + }, +}; + +use crate::elgamal::ECGFP5SecretKey; + +pub struct ECGFP5SecretKeyTarget(pub BigUInt320Target); + +pub type PublicKeyTarget = PointTarget; + +pub trait CircuitBuilderECGFP5PublicKey<F: RichField + Extendable<D>, const D: usize> { + /// Adds secret key target + fn add_secret_key(&mut self) -> ECGFP5SecretKeyTarget; + + /// Derives public key from secret key + fn public_key(&mut self, sk: &ECGFP5SecretKeyTarget) -> PublicKeyTarget; +} + +pub trait CircuitBuilderElGamal<F: RichField + Extendable<D>, const D: usize> { + /// Encrypts an EcGFp5 point using a public key and nonce. + fn elgamal_encrypt( + &mut self, + pk: &PublicKeyTarget, + nonce: &BigUInt320Target, + msg: &PointTarget, + ) -> (PointTarget, PointTarget); +} + +impl CircuitBuilderECGFP5PublicKey<F, D> for CircuitBuilder<F, D> { + fn add_secret_key(&mut self) -> ECGFP5SecretKeyTarget { + ECGFP5SecretKeyTarget(self.add_virtual_biguint320_target()) + } + + fn public_key(&mut self, sk: &ECGFP5SecretKeyTarget) -> PublicKeyTarget { + let generator = self.constant_point(Point::generator()); + self.multiply_point(&sk.0.bits, &generator) + } +} + +impl CircuitBuilderElGamal<F, D> for CircuitBuilder<F, D> { + fn elgamal_encrypt( + &mut self, + pk: &PublicKeyTarget, + nonce: &BigUInt320Target, + msg: &PointTarget, + ) -> (PointTarget, PointTarget) { + let generator = self.constant_point(Point::generator()); + let nonce_times_gen = self.multiply_point(&nonce.bits, &generator); + let nonce_times_pk = self.multiply_point(&nonce.bits, pk); + (nonce_times_gen, self.add_point(msg, &nonce_times_pk)) + } +} + +pub trait PartialWitnessECGFP5PublicKey { + fn set_secret_key_target( + &mut self, + target: &ECGFP5SecretKeyTarget, + value: &ECGFP5SecretKey, + ) -> anyhow::Result<()>; +} + +impl PartialWitnessECGFP5PublicKey for PartialWitness<F> { + fn set_secret_key_target( + &mut self, + target: &ECGFP5SecretKeyTarget, + value: &ECGFP5SecretKey, + ) -> anyhow::Result<()> { + self.set_biguint320_target(&target.0, &value.0) + } +} + +#[cfg(test)] +mod tests { + use num::BigUint; + use num_bigint::RandBigInt; + use plonky2::{ + iop::witness::PartialWitness, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, + }; + use pod2::backends::plonky2::{ + basetypes::{D, F}, + primitives::ec::{ + bits::CircuitBuilderBits, + curve::{CircuitBuilderElliptic, GROUP_ORDER, Point, WitnessWriteCurve}, + }, + }; + use rand::rngs::OsRng; + + use crate::elgamal::{ + ECGFP5SecretKey, TOTAL_BITS, + circuit::{ + CircuitBuilderECGFP5PublicKey, CircuitBuilderElGamal, PartialWitnessECGFP5PublicKey, + }, + elgamal_decrypt, elgamal_encrypt, + }; + + #[test] + fn public_key_calculation() -> anyhow::Result<()> { + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let sk_target = builder.add_secret_key(); + let pk_target = builder.public_key(&sk_target); + + let data = builder.build::<PoseidonGoldilocksConfig>(); + (0..10).try_for_each(|_| { + let sk = ECGFP5SecretKey::rand(); + let pk = sk.public_key(); + + let mut pw = PartialWitness::new(); + pw.set_secret_key_target(&sk_target, &sk)?; + pw.set_point_target(&pk_target, &pk)?; + + let proof = data.prove(pw)?; + data.verify(proof) + }) + } + + #[test] + fn elgamal_encryption() -> anyhow::Result<()> { + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let pk_target = builder.add_virtual_point_target(); + let nonce_target = builder.add_virtual_biguint320_target(); + let msg_target = builder.add_virtual_point_target(); + + let ct_target = builder.elgamal_encrypt(&pk_target, &nonce_target, &msg_target); + + let data = builder.build::<PoseidonGoldilocksConfig>(); + + (0..10).try_for_each(|_| { + let sk = ECGFP5SecretKey::rand(); + let pk = sk.public_key(); + + let msg = Point::new_rand_from_subgroup(); + let nonce = OsRng.gen_biguint_below(&GROUP_ORDER); + + let encrypted_msg = elgamal_encrypt(pk, &nonce, msg); + + let mut pw = PartialWitness::new(); + pw.set_point_target(&pk_target, &pk)?; + pw.set_point_target(&msg_target, &msg)?; + pw.set_biguint320_target(&nonce_target, &nonce)?; + pw.set_point_target(&ct_target.0, &encrypted_msg.0)?; + pw.set_point_target(&ct_target.1, &encrypted_msg.1)?; + + let proof = data.prove(pw)?; + data.verify(proof) + }) + } +}
A ecgfp5/src/lib.rs

@@ -0,0 +1,1 @@

+pub mod elgamal;