Skip to main content

darkpool_client/
crypto_helpers.rs

1//! Note-specific crypto: packing, encryption, nullifier derivation.
2//! Re-exports pure primitives from `darkpool_crypto`.
3
4use ethers::types::U256;
5
6// Re-export all pure crypto functions from darkpool-crypto for backward compatibility
7pub use darkpool_crypto::{
8    address_to_field, aes128_decrypt, aes128_encrypt, bjj_is_on_curve, bjj_scalar_mul,
9    derive_public_key_from_sk, derive_shared_secret_bjj, field_to_address, fr_to_u256,
10    from_noir_hex, kdf_to_aes_key_iv, poseidon_hash, random_bjj_scalar, random_field, string_to_fr,
11    to_noir_decimal, to_noir_hex, u256_to_fr, CryptoError,
12};
13
14use crate::proof_inputs::NotePlaintext;
15
16/// `NullifierHash` = Poseidon(`noteNullifier`)
17#[must_use]
18pub fn derive_nullifier_path_a(note_nullifier: U256) -> U256 {
19    poseidon_hash(&[note_nullifier])
20}
21
22/// `NullifierHash` = Poseidon(`sharedSecret`, commitment, `leafIndex`)
23#[must_use]
24pub fn derive_nullifier_path_b(shared_secret: U256, commitment: U256, leaf_index: u64) -> U256 {
25    poseidon_hash(&[shared_secret, commitment, U256::from(leaf_index)])
26}
27
28#[must_use]
29pub fn calculate_public_memo_id(
30    value: U256,
31    asset_id: U256,
32    timelock: U256,
33    owner_x: U256,
34    owner_y: U256,
35    salt: U256,
36) -> U256 {
37    poseidon_hash(&[value, asset_id, timelock, owner_x, owner_y, salt])
38}
39
40/// Pack a `NotePlaintext` into 192 bytes (6 × 32-byte BE fields).
41#[must_use]
42pub fn pack_note_plaintext(note: &NotePlaintext) -> [u8; 192] {
43    let mut buf = [0u8; 192];
44    let mut offset = 0;
45
46    for field in [
47        note.asset_id,
48        note.value,
49        note.secret,
50        note.nullifier,
51        note.timelock,
52        note.hashlock,
53    ] {
54        field.to_big_endian(&mut buf[offset..offset + 32]);
55        offset += 32;
56    }
57
58    buf
59}
60
61#[must_use]
62pub fn unpack_note_plaintext(bytes: &[u8; 192]) -> NotePlaintext {
63    let asset_id = U256::from_big_endian(&bytes[0..32]);
64    let value = U256::from_big_endian(&bytes[32..64]);
65    let secret = U256::from_big_endian(&bytes[64..96]);
66    let nullifier = U256::from_big_endian(&bytes[96..128]);
67    let timelock = U256::from_big_endian(&bytes[128..160]);
68    let hashlock = U256::from_big_endian(&bytes[160..192]);
69
70    NotePlaintext {
71        value,
72        asset_id,
73        secret,
74        nullifier,
75        timelock,
76        hashlock,
77    }
78}
79
80/// Pack 208-byte ciphertext into 7 field elements (LE, 31+31+31+31+31+31+22).
81#[must_use]
82pub fn pack_ciphertext_to_fields(ciphertext: &[u8; 208]) -> [U256; 7] {
83    let mut fields = [U256::zero(); 7];
84    let mut idx = 0;
85
86    for (p, field) in fields.iter_mut().enumerate() {
87        let bytes_in_this = if p < 6 { 31 } else { 22 };
88        let mut val = U256::zero();
89
90        for i in (0..bytes_in_this).rev() {
91            val <<= 8;
92            val += U256::from(ciphertext[idx + i]);
93        }
94
95        *field = val;
96        idx += bytes_in_this;
97    }
98
99    fields
100}
101
102#[must_use]
103pub fn unpack_ciphertext_from_fields(packed: &[U256; 7]) -> [u8; 208] {
104    let mut ciphertext = [0u8; 208];
105    let mut idx = 0;
106
107    for (p, &val) in packed.iter().enumerate() {
108        let mut val = val;
109        let bytes_in_this = if p < 6 { 31 } else { 22 };
110
111        for _ in 0..bytes_in_this {
112            ciphertext[idx] = (val % U256::from(256)).as_u32() as u8;
113            val /= U256::from(256);
114            idx += 1;
115        }
116    }
117
118    ciphertext
119}
120
121/// Encrypt a note for deposit. Returns (`packed_fields`, `ephemeral_pk`).
122pub fn encrypt_note_for_deposit_aes(
123    ephemeral_sk: U256,
124    compliance_pk: (U256, U256),
125    note: &NotePlaintext,
126) -> Result<([U256; 7], (U256, U256)), CryptoError> {
127    let shared_x = derive_shared_secret_bjj(ephemeral_sk, compliance_pk)?;
128    let (key, iv) = kdf_to_aes_key_iv(shared_x);
129    let plaintext = pack_note_plaintext(note);
130    let ciphertext = aes128_encrypt(&plaintext, &key, &iv);
131    let fields = pack_ciphertext_to_fields(&ciphertext);
132    let epk = derive_public_key_from_sk(ephemeral_sk)?;
133    Ok((fields, epk))
134}
135
136pub struct MemoEncryptionResult {
137    pub packed_ciphertext: [U256; 7],
138    pub ephemeral_pk: (U256, U256),
139    /// a * `compliance_pk` -- Bob uses `b * int_bob` to decrypt
140    pub int_bob: (U256, U256),
141    /// a * `recipient_b` -- Carol uses `c * int_carol` to decrypt
142    pub int_carol: (U256, U256),
143}
144
145/// 3-party ECDH memo encryption: S = a * b * c * G shared between
146/// sender (a), recipient (b/ivk), and compliance (c).
147pub fn encrypt_memo_note_3party(
148    ephemeral_sk: U256,
149    recipient_p: (U256, U256),   // P = ivk * compliance_pk = b * c * G
150    recipient_b: (U256, U256),   // B = ivk * G = b * G
151    compliance_pk: (U256, U256), // C = c * G
152    note: &NotePlaintext,
153) -> Result<MemoEncryptionResult, CryptoError> {
154    let s_point = bjj_scalar_mul(ephemeral_sk, recipient_p)?;
155    let shared_secret = s_point.0;
156
157    let (key, iv) = kdf_to_aes_key_iv(shared_secret);
158    let plaintext = pack_note_plaintext(note);
159    let ciphertext = aes128_encrypt(&plaintext, &key, &iv);
160    let packed = pack_ciphertext_to_fields(&ciphertext);
161
162    let epk = derive_public_key_from_sk(ephemeral_sk)?;
163    let int_bob = bjj_scalar_mul(ephemeral_sk, compliance_pk)?;
164    let int_carol = bjj_scalar_mul(ephemeral_sk, recipient_b)?;
165
166    Ok(MemoEncryptionResult {
167        packed_ciphertext: packed,
168        ephemeral_pk: epk,
169        int_bob,
170        int_carol,
171    })
172}
173
174pub fn decrypt_note_from_fields(
175    packed: &[U256; 7],
176    ephemeral_sk: U256,
177    compliance_pk: (U256, U256),
178) -> Result<NotePlaintext, CryptoError> {
179    let ciphertext = unpack_ciphertext_from_fields(packed);
180    let shared_x = derive_shared_secret_bjj(ephemeral_sk, compliance_pk)?;
181    let (key, iv) = kdf_to_aes_key_iv(shared_x);
182    let plaintext = aes128_decrypt(&ciphertext, &key, &iv)?;
183    Ok(unpack_note_plaintext(&plaintext))
184}
185
186pub struct DleqResult {
187    pub recipient_b: (U256, U256),
188    pub recipient_p: (U256, U256),
189    pub proof: crate::proof_inputs::DLEQProof,
190}
191
192pub fn generate_dleq_proof(
193    recipient_sk: U256,
194    compliance_pk: (U256, U256),
195) -> Result<DleqResult, CryptoError> {
196    let raw = darkpool_crypto::generate_dleq_proof(recipient_sk, compliance_pk)?;
197    Ok(DleqResult {
198        recipient_b: raw.recipient_b,
199        recipient_p: raw.recipient_p,
200        proof: crate::proof_inputs::DLEQProof {
201            u: raw.u,
202            v: raw.v,
203            z: raw.z,
204        },
205    })
206}
207
208/// Returns (note, `shared_secret`) -- `shared_secret` is needed for Path B nullifier derivation.
209pub fn recipient_decrypt_3party(
210    recipient_sk: U256,
211    intermediate_point: (U256, U256),
212    packed_ciphertext: &[U256; 7],
213) -> Result<(NotePlaintext, U256), CryptoError> {
214    let shared_point = bjj_scalar_mul(recipient_sk, intermediate_point)?;
215    let shared_secret = shared_point.0;
216    let (key, iv) = kdf_to_aes_key_iv(shared_secret);
217    let ciphertext = unpack_ciphertext_from_fields(packed_ciphertext);
218    let plaintext = aes128_decrypt(&ciphertext, &key, &iv)?;
219    Ok((unpack_note_plaintext(&plaintext), shared_secret))
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_nullifier_derivation() {
228        let nullifier = U256::from(12345);
229        let hash_a = derive_nullifier_path_a(nullifier);
230        assert!(!hash_a.is_zero());
231
232        let shared = U256::from(111);
233        let commitment = U256::from(222);
234        let hash_b = derive_nullifier_path_b(shared, commitment, 5);
235        assert!(!hash_b.is_zero());
236        assert_ne!(hash_a, hash_b);
237    }
238
239    #[test]
240    fn test_note_plaintext_packing_roundtrip() {
241        let note = NotePlaintext {
242            asset_id: U256::from(1),
243            value: U256::from(1000),
244            secret: U256::from(12345),
245            nullifier: U256::from(67890),
246            timelock: U256::from(0),
247            hashlock: U256::from(0),
248        };
249
250        let packed = pack_note_plaintext(&note);
251        assert_eq!(packed.len(), 192);
252
253        let unpacked = unpack_note_plaintext(&packed);
254        assert_eq!(unpacked.asset_id, note.asset_id);
255        assert_eq!(unpacked.value, note.value);
256        assert_eq!(unpacked.secret, note.secret);
257        assert_eq!(unpacked.nullifier, note.nullifier);
258        assert_eq!(unpacked.timelock, note.timelock);
259        assert_eq!(unpacked.hashlock, note.hashlock);
260    }
261
262    #[test]
263    fn test_ciphertext_field_packing_roundtrip() {
264        // Create a 208-byte ciphertext with known pattern
265        let mut ciphertext = [0u8; 208];
266        for (i, byte) in ciphertext.iter_mut().enumerate() {
267            *byte = (i % 256) as u8;
268        }
269
270        let fields = pack_ciphertext_to_fields(&ciphertext);
271        assert_eq!(fields.len(), 7);
272
273        let unpacked = unpack_ciphertext_from_fields(&fields);
274        assert_eq!(unpacked, ciphertext);
275    }
276
277    #[test]
278    fn test_ciphertext_field_sizes() {
279        let ciphertext = [0xffu8; 208];
280        let fields = pack_ciphertext_to_fields(&ciphertext);
281
282        // Fields 0-5 should hold 31 bytes each (max value < 2^248)
283        // Field 6 should hold 22 bytes (max value < 2^176)
284        for (i, field) in fields[..6].iter().enumerate() {
285            assert!(
286                !field.is_zero(),
287                "Field {} should not be zero for 0xff input",
288                i
289            );
290        }
291        assert!(!fields[6].is_zero());
292    }
293
294    #[test]
295    fn test_full_note_encryption_decryption() {
296        use darkpool_crypto::BASE8;
297
298        // Create a compliance keypair
299        let compliance_sk = U256::from(987654321u64);
300        let mut sk_bytes = [0u8; 32];
301        compliance_sk.to_big_endian(&mut sk_bytes);
302        sk_bytes.reverse();
303        let compliance_pk_point = BASE8.mul_scalar(&sk_bytes).expect("valid test key");
304        let compliance_pk = (
305            fr_to_u256(compliance_pk_point.x()),
306            fr_to_u256(compliance_pk_point.y()),
307        );
308
309        // Create a note
310        let note = NotePlaintext {
311            asset_id: U256::from(0x123456789abcdef0u64),
312            value: U256::from(1_000_000_000_000_000_000u64), // 1 ETH
313            secret: random_field(),
314            nullifier: random_field(),
315            timelock: U256::zero(),
316            hashlock: U256::zero(),
317        };
318
319        // Ephemeral key
320        let ephemeral_sk = U256::from(12345678u64);
321
322        // Encrypt
323        let (packed_fields, epk) = encrypt_note_for_deposit_aes(ephemeral_sk, compliance_pk, &note)
324            .expect("encryption should succeed");
325
326        assert_eq!(packed_fields.len(), 7);
327        assert!(!epk.0.is_zero() || !epk.1.is_zero());
328
329        // Decrypt
330        let decrypted = decrypt_note_from_fields(&packed_fields, ephemeral_sk, compliance_pk)
331            .expect("decryption should succeed");
332
333        assert_eq!(decrypted.asset_id, note.asset_id);
334        assert_eq!(decrypted.value, note.value);
335        assert_eq!(decrypted.secret, note.secret);
336        assert_eq!(decrypted.nullifier, note.nullifier);
337        assert_eq!(decrypted.timelock, note.timelock);
338        assert_eq!(decrypted.hashlock, note.hashlock);
339    }
340}