Skip to main content

darkpool_crypto/
dleq.rs

1//! Chaum-Pedersen NIZK proof that `log_G(B) == log_C(P)` for compliance key derivation.
2
3use ark_ff::{BigInteger, PrimeField};
4use ethers_core::types::U256;
5
6use crate::bjj::{PublicKey, BASE8};
7use crate::error::CryptoError;
8use crate::field::{fr_to_u256, poseidon_hash, random_field, u256_to_fr};
9
10/// BJJ scalar field type (native field arithmetic, zero heap allocation).
11type BjjFr = ark_ed_on_bn254::Fr;
12
13pub struct RawDleqProof {
14    pub recipient_b: (U256, U256),
15    pub recipient_p: (U256, U256),
16    pub u: (U256, U256),
17    pub v: (U256, U256),
18    pub z: U256,
19}
20
21/// Generate a Chaum-Pedersen DLEQ NIZK proof that `log_G(B) == log_C(P)`.
22pub fn generate_dleq_proof(
23    recipient_sk: U256,
24    compliance_pk: (U256, U256),
25) -> Result<RawDleqProof, CryptoError> {
26    let mut b_bytes_be = [0u8; 32];
27    recipient_sk.to_big_endian(&mut b_bytes_be);
28    let b_fr = BjjFr::from_be_bytes_mod_order(&b_bytes_be);
29    let b_le = b_fr.into_bigint().to_bytes_le();
30
31    // B = [b] * G
32    let b_point = BASE8.mul_scalar(&b_le)?;
33    let b_result = point_to_u256_pair(&b_point);
34
35    // P = [b] * C
36    let c_point =
37        PublicKey::from_coordinates(u256_to_fr(compliance_pk.0), u256_to_fr(compliance_pk.1))?;
38    let p_point = c_point.mul_scalar(&b_le)?;
39    let p_result = point_to_u256_pair(&p_point);
40
41    let r_fr = {
42        let r_u256 = random_field();
43        let mut r_bytes = [0u8; 32];
44        r_u256.to_big_endian(&mut r_bytes);
45        BjjFr::from_be_bytes_mod_order(&r_bytes)
46    };
47    let r_le = r_fr.into_bigint().to_bytes_le();
48
49    // U = [r] * G
50    let u_point = BASE8.mul_scalar(&r_le)?;
51    let u_result = point_to_u256_pair(&u_point);
52
53    // V = [r] * C
54    let v_point = c_point.mul_scalar(&r_le)?;
55    let v_result = point_to_u256_pair(&v_point);
56
57    let g_x = fr_to_u256(BASE8.x());
58    let g_y = fr_to_u256(BASE8.y());
59
60    let e_u256 = poseidon_hash(&[
61        u_result.0,
62        u_result.1,
63        v_result.0,
64        v_result.1,
65        g_x,
66        g_y,
67        compliance_pk.0,
68        compliance_pk.1,
69        b_result.0,
70        b_result.1,
71        p_result.0,
72        p_result.1,
73    ]);
74
75    let mut e_bytes = [0u8; 32];
76    e_u256.to_big_endian(&mut e_bytes);
77    let e_fr = BjjFr::from_be_bytes_mod_order(&e_bytes);
78
79    let z_fr = r_fr + e_fr * b_fr;
80    let z_be = z_fr.into_bigint().to_bytes_be();
81    let z_u256 = U256::from_big_endian(&z_be);
82
83    Ok(RawDleqProof {
84        recipient_b: b_result,
85        recipient_p: p_result,
86        u: u_result,
87        v: v_result,
88        z: z_u256,
89    })
90}
91
92fn point_to_u256_pair(point: &PublicKey) -> (U256, U256) {
93    (fr_to_u256(point.x()), fr_to_u256(point.y()))
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::bjj::SecretKey;
100
101    fn bjj_fr_to_le_bytes(fr: &BjjFr) -> Vec<u8> {
102        fr.into_bigint().to_bytes_le()
103    }
104
105    #[test]
106    fn test_dleq_proof_generation_and_verification() {
107        let mut rng = rand::thread_rng();
108
109        let compliance_sk = SecretKey::generate(&mut rng);
110        let compliance_pk = compliance_sk.public_key().expect("compliance pk");
111        let compliance_pk_tuple = (fr_to_u256(compliance_pk.x()), fr_to_u256(compliance_pk.y()));
112
113        let recipient_sk_val = random_field();
114
115        let proof = generate_dleq_proof(recipient_sk_val, compliance_pk_tuple)
116            .expect("DLEQ proof generation should succeed");
117
118        // Verify: [z]*G == U + [e]*B and [z]*C == V + [e]*P
119        let g_x = fr_to_u256(BASE8.x());
120        let g_y = fr_to_u256(BASE8.y());
121        let e_u256 = poseidon_hash(&[
122            proof.u.0,
123            proof.u.1,
124            proof.v.0,
125            proof.v.1,
126            g_x,
127            g_y,
128            compliance_pk_tuple.0,
129            compliance_pk_tuple.1,
130            proof.recipient_b.0,
131            proof.recipient_b.1,
132            proof.recipient_p.0,
133            proof.recipient_p.1,
134        ]);
135
136        let mut e_bytes = [0u8; 32];
137        e_u256.to_big_endian(&mut e_bytes);
138        let e_fr = BjjFr::from_be_bytes_mod_order(&e_bytes);
139        let e_le = bjj_fr_to_le_bytes(&e_fr);
140
141        let mut z_bytes = [0u8; 32];
142        proof.z.to_big_endian(&mut z_bytes);
143        let z_fr = BjjFr::from_be_bytes_mod_order(&z_bytes);
144        let z_le = bjj_fr_to_le_bytes(&z_fr);
145        let z_g = BASE8.mul_scalar(&z_le).expect("[z]*G");
146        let b_point = PublicKey::new_unchecked(
147            u256_to_fr(proof.recipient_b.0),
148            u256_to_fr(proof.recipient_b.1),
149        );
150        let e_b = b_point.mul_scalar(&e_le).expect("[e]*B");
151        let u_point = PublicKey::new_unchecked(u256_to_fr(proof.u.0), u256_to_fr(proof.u.1));
152        let u_plus_eb = u_point.add(&e_b).expect("U + [e]*B");
153        assert_eq!(
154            z_g.x(),
155            u_plus_eb.x(),
156            "DLEQ check 1 failed: [z]*G.x != (U + [e]*B).x"
157        );
158        assert_eq!(
159            z_g.y(),
160            u_plus_eb.y(),
161            "DLEQ check 1 failed: [z]*G.y != (U + [e]*B).y"
162        );
163
164        let c_point = PublicKey::new_unchecked(
165            u256_to_fr(compliance_pk_tuple.0),
166            u256_to_fr(compliance_pk_tuple.1),
167        );
168        let z_c = c_point.mul_scalar(&z_le).expect("[z]*C");
169        let p_point = PublicKey::new_unchecked(
170            u256_to_fr(proof.recipient_p.0),
171            u256_to_fr(proof.recipient_p.1),
172        );
173        let e_p = p_point.mul_scalar(&e_le).expect("[e]*P");
174        let v_point = PublicKey::new_unchecked(u256_to_fr(proof.v.0), u256_to_fr(proof.v.1));
175        let v_plus_ep = v_point.add(&e_p).expect("V + [e]*P");
176        assert_eq!(
177            z_c.x(),
178            v_plus_ep.x(),
179            "DLEQ check 2 failed: [z]*C.x != (V + [e]*P).x"
180        );
181        assert_eq!(
182            z_c.y(),
183            v_plus_ep.y(),
184            "DLEQ check 2 failed: [z]*C.y != (V + [e]*P).y"
185        );
186    }
187
188    #[test]
189    fn test_dleq_wrong_witness_fails() {
190        let mut rng = rand::thread_rng();
191
192        let compliance_sk = SecretKey::generate(&mut rng);
193        let compliance_pk = compliance_sk.public_key().expect("compliance pk");
194        let compliance_pk_tuple = (fr_to_u256(compliance_pk.x()), fr_to_u256(compliance_pk.y()));
195
196        let recipient_sk_val = random_field();
197        let proof = generate_dleq_proof(recipient_sk_val, compliance_pk_tuple)
198            .expect("proof generation must succeed");
199
200        let tampered_z = proof.z + ethers_core::types::U256::from(1u64);
201        let g_x = fr_to_u256(BASE8.x());
202        let g_y = fr_to_u256(BASE8.y());
203        let e_u256 = poseidon_hash(&[
204            proof.u.0,
205            proof.u.1,
206            proof.v.0,
207            proof.v.1,
208            g_x,
209            g_y,
210            compliance_pk_tuple.0,
211            compliance_pk_tuple.1,
212            proof.recipient_b.0,
213            proof.recipient_b.1,
214            proof.recipient_p.0,
215            proof.recipient_p.1,
216        ]);
217
218        let mut e_bytes = [0u8; 32];
219        e_u256.to_big_endian(&mut e_bytes);
220        let e_fr = BjjFr::from_be_bytes_mod_order(&e_bytes);
221        let e_le = bjj_fr_to_le_bytes(&e_fr);
222
223        let mut tz_bytes = [0u8; 32];
224        tampered_z.to_big_endian(&mut tz_bytes);
225        let tz_fr = BjjFr::from_be_bytes_mod_order(&tz_bytes);
226        let tz_le = bjj_fr_to_le_bytes(&tz_fr);
227
228        let zp_g = BASE8.mul_scalar(&tz_le).expect("[z']*G");
229        let b_point = PublicKey::new_unchecked(
230            u256_to_fr(proof.recipient_b.0),
231            u256_to_fr(proof.recipient_b.1),
232        );
233        let e_b = b_point.mul_scalar(&e_le).expect("[e]*B");
234        let u_point = PublicKey::new_unchecked(u256_to_fr(proof.u.0), u256_to_fr(proof.u.1));
235        let u_plus_eb = u_point.add(&e_b).expect("U + [e]*B");
236
237        assert!(
238            zp_g.x() != u_plus_eb.x() || zp_g.y() != u_plus_eb.y(),
239            "Tampered z must fail DLEQ verification -- soundness violation!"
240        );
241    }
242}