1use ethers::types::U256;
6use tracing::trace;
7
8use crate::crypto_helpers::{
9 decrypt_note_from_fields, derive_nullifier_path_a, derive_nullifier_path_b,
10 recipient_decrypt_3party,
11};
12use crate::key_repository::KeyRepository;
13use crate::proof_inputs::NotePlaintext;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum EventType {
17 NewNote,
18 NewMemo,
19}
20
21#[derive(Debug, Clone)]
22pub struct UnprocessedEvent {
23 pub event_type: EventType,
24 pub block_number: u64,
25 pub tx_hash: String,
26 pub leaf_index: u64,
27 pub commitment: U256,
28 pub epk_x: U256,
29 pub epk_y: U256,
30 pub packed_ciphertext: [U256; 7],
31 pub tag: Option<U256>,
32 pub intermediate_bob_x: Option<U256>,
33 pub intermediate_bob_y: Option<U256>,
34}
35
36#[derive(Debug, Clone)]
37pub struct WalletNote {
38 pub note: NotePlaintext,
39 pub commitment: U256,
40 pub leaf_index: u64,
41 pub nullifier: U256,
42 pub spending_secret: U256,
44 pub is_transfer: bool,
45 pub derivation_index: u64,
46 pub spent: bool,
47}
48
49pub struct NoteProcessor<'a> {
50 key_repo: &'a KeyRepository,
51 compliance_pk: (U256, U256),
52}
53
54impl<'a> NoteProcessor<'a> {
55 #[must_use]
56 pub fn new(key_repo: &'a KeyRepository, compliance_pk: (U256, U256)) -> Self {
57 Self {
58 key_repo,
59 compliance_pk,
60 }
61 }
62
63 #[must_use]
64 pub fn process(&self, event: &UnprocessedEvent) -> Option<WalletNote> {
65 match event.event_type {
66 EventType::NewNote => self.process_new_note(event),
67 EventType::NewMemo => self.process_memo(event),
68 }
69 }
70
71 fn process_new_note(&self, event: &UnprocessedEvent) -> Option<WalletNote> {
72 let (ephemeral_sk, derivation_index) =
73 self.key_repo.try_match_deposit(event.epk_x, event.epk_y)?;
74
75 let note = match decrypt_note_from_fields(
76 &event.packed_ciphertext,
77 ephemeral_sk,
78 self.compliance_pk,
79 ) {
80 Ok(n) => n,
81 Err(e) => {
82 trace!(
83 "NewNote decryption failed for leaf_index={}, block={}, tx={}: {:?}",
84 event.leaf_index,
85 event.block_number,
86 event.tx_hash,
87 e
88 );
89 return None;
90 }
91 };
92
93 let nullifier = derive_nullifier_path_a(note.nullifier);
94
95 Some(WalletNote {
96 note,
97 commitment: event.commitment,
98 leaf_index: event.leaf_index,
99 nullifier,
100 spending_secret: ephemeral_sk,
101 is_transfer: false,
102 derivation_index,
103 spent: false,
104 })
105 }
106
107 fn process_memo(&self, event: &UnprocessedEvent) -> Option<WalletNote> {
108 let tag = event.tag?;
109 let int_bob_x = event.intermediate_bob_x?;
110 let int_bob_y = event.intermediate_bob_y?;
111
112 let (recipient_sk_mod, derivation_index) = self.key_repo.try_match_transfer(tag)?;
113 let intermediate_point = (int_bob_x, int_bob_y);
114 let (note, shared_secret) = match recipient_decrypt_3party(
115 recipient_sk_mod,
116 intermediate_point,
117 &event.packed_ciphertext,
118 ) {
119 Ok((n, ss)) => (n, ss),
120 Err(e) => {
121 trace!(
122 "NewMemo decryption failed for leaf_index={}, block={}, tx={}: {:?}",
123 event.leaf_index,
124 event.block_number,
125 event.tx_hash,
126 e
127 );
128 return None;
129 }
130 };
131
132 let nullifier = derive_nullifier_path_b(shared_secret, event.commitment, event.leaf_index);
133
134 Some(WalletNote {
135 note,
136 commitment: event.commitment,
137 leaf_index: event.leaf_index,
138 nullifier,
139 spending_secret: shared_secret,
140 is_transfer: true,
141 derivation_index,
142 spent: false,
143 })
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::crypto_helpers::{
151 aes128_encrypt, encrypt_note_for_deposit_aes, fr_to_u256, kdf_to_aes_key_iv,
152 pack_ciphertext_to_fields, pack_note_plaintext, random_field,
153 };
154 use crate::identity::DarkAccount;
155 use darkpool_crypto::BASE8;
156
157 fn create_compliance_keypair() -> (U256, (U256, U256)) {
158 let compliance_sk_bytes = [0x42u8; 32];
159 let compliance_pk_point = BASE8
160 .mul_scalar(&compliance_sk_bytes)
161 .expect("valid test key");
162 let compliance_pk = (
163 fr_to_u256(compliance_pk_point.x()),
164 fr_to_u256(compliance_pk_point.y()),
165 );
166 (U256::from_big_endian(&compliance_sk_bytes), compliance_pk)
167 }
168
169 fn create_test_note() -> NotePlaintext {
170 NotePlaintext {
171 asset_id: U256::from(1),
172 value: U256::from(1_000_000_000_000_000_000u64), secret: random_field(),
174 nullifier: random_field(),
175 timelock: U256::zero(),
176 hashlock: U256::zero(),
177 }
178 }
179
180 #[test]
181 fn test_path_a_deposit_decryption() {
182 let account = DarkAccount::from_seed(b"test_path_a_seed");
183 let (_, compliance_pk) = create_compliance_keypair();
184 let mut key_repo = KeyRepository::new(account, compliance_pk);
185 let (ephemeral_sk, _nonce) = key_repo.next_ephemeral_params();
186 let note = create_test_note();
187
188 let (packed_ciphertext, epk) =
189 encrypt_note_for_deposit_aes(ephemeral_sk, compliance_pk, ¬e)
190 .expect("encryption should succeed");
191 let commitment = crate::crypto_helpers::poseidon_hash(&packed_ciphertext);
192
193 let event = UnprocessedEvent {
194 event_type: EventType::NewNote,
195 block_number: 100,
196 tx_hash: "0x1234".to_string(),
197 leaf_index: 0,
198 commitment,
199 epk_x: epk.0,
200 epk_y: epk.1,
201 packed_ciphertext,
202 tag: None,
203 intermediate_bob_x: None,
204 intermediate_bob_y: None,
205 };
206
207 let processor = NoteProcessor::new(&key_repo, compliance_pk);
208 let wallet_note = processor
209 .process(&event)
210 .expect("should decrypt our deposit");
211 assert_eq!(wallet_note.note.asset_id, note.asset_id);
212 assert_eq!(wallet_note.note.value, note.value);
213 assert_eq!(wallet_note.note.secret, note.secret);
214 assert_eq!(wallet_note.note.nullifier, note.nullifier);
215 assert!(!wallet_note.is_transfer);
216 assert_eq!(wallet_note.derivation_index, 0);
217 assert!(!wallet_note.spent);
218 }
219
220 #[test]
221 fn test_path_a_unknown_epk_returns_none() {
222 let account = DarkAccount::from_seed(b"test_path_a_unknown");
223 let (_, compliance_pk) = create_compliance_keypair();
224 let key_repo = KeyRepository::new(account, compliance_pk);
225
226 let event = UnprocessedEvent {
227 event_type: EventType::NewNote,
228 block_number: 100,
229 tx_hash: "0x1234".to_string(),
230 leaf_index: 0,
231 commitment: U256::from(12345),
232 epk_x: U256::from(999999), epk_y: U256::from(888888),
234 packed_ciphertext: [U256::zero(); 7],
235 tag: None,
236 intermediate_bob_x: None,
237 intermediate_bob_y: None,
238 };
239
240 let processor = NoteProcessor::new(&key_repo, compliance_pk);
241 let result = processor.process(&event);
242
243 assert!(result.is_none(), "Should not match unknown ephemeral PK");
244 }
245
246 #[test]
247 fn test_path_b_transfer_decryption() {
248 use num_bigint::BigUint;
249
250 fn reduce_mod_subgroup(value: U256) -> U256 {
252 let mut bytes = [0u8; 32];
253 value.to_big_endian(&mut bytes);
254 let bigint = BigUint::from_bytes_be(&bytes);
255 let order = BigUint::parse_bytes(darkpool_crypto::SUBGROUP_ORDER.as_bytes(), 10)
256 .expect("valid subgroup order");
257 let reduced = bigint % order;
258 let mut result_bytes = reduced.to_bytes_be();
259 while result_bytes.len() < 32 {
260 result_bytes.insert(0, 0);
261 }
262 U256::from_big_endian(&result_bytes)
263 }
264
265 let mut recipient_account = DarkAccount::from_seed(b"test_path_b_recipient");
266 let (_, compliance_pk) = create_compliance_keypair();
267 let mut key_repo = KeyRepository::new(recipient_account.clone(), compliance_pk);
268 key_repo.advance_incoming_keys(5);
269
270 let recipient_ivk = recipient_account.get_incoming_viewing_key(0);
272 let recipient_ivk_mod = reduce_mod_subgroup(recipient_ivk);
273
274 let mut ivk_mod_bytes = [0u8; 32];
275 recipient_ivk_mod.to_big_endian(&mut ivk_mod_bytes);
276 ivk_mod_bytes.reverse();
277 let recipient_pk_from_mod = BASE8.mul_scalar(&ivk_mod_bytes).expect("valid test key");
278
279 let sender_r = U256::from(98765u64);
280 let mut r_bytes = [0u8; 32];
281 sender_r.to_big_endian(&mut r_bytes);
282 r_bytes.reverse();
283
284 let intermediate_bob_point = BASE8.mul_scalar(&r_bytes).expect("valid test key");
285 let intermediate_bob = (
286 fr_to_u256(intermediate_bob_point.x()),
287 fr_to_u256(intermediate_bob_point.y()),
288 );
289
290 let sender_shared_point = recipient_pk_from_mod
291 .mul_scalar(&r_bytes)
292 .expect("valid test key");
293 let sender_shared_secret = fr_to_u256(sender_shared_point.x());
294
295 let note = create_test_note();
296 let (key, iv) = kdf_to_aes_key_iv(sender_shared_secret);
297 let plaintext = pack_note_plaintext(¬e);
298
299 let ciphertext = aes128_encrypt(&plaintext, &key, &iv);
300 let packed_ciphertext = pack_ciphertext_to_fields(&ciphertext);
301
302 use darkpool_crypto::PublicKey;
303 let compliance_pk_point = PublicKey::new_unchecked(
304 crate::crypto_helpers::u256_to_fr(compliance_pk.0),
305 crate::crypto_helpers::u256_to_fr(compliance_pk.1),
306 );
307 let tag_point = compliance_pk_point
308 .mul_scalar(&ivk_mod_bytes)
309 .expect("valid test key");
310 let tag = fr_to_u256(tag_point.x());
311
312 let commitment = crate::crypto_helpers::poseidon_hash(&packed_ciphertext);
313
314 let event = UnprocessedEvent {
315 event_type: EventType::NewMemo,
316 block_number: 200,
317 tx_hash: "0x5678".to_string(),
318 leaf_index: 10,
319 commitment,
320 epk_x: U256::zero(),
321 epk_y: U256::zero(),
322 packed_ciphertext,
323 tag: Some(tag),
324 intermediate_bob_x: Some(intermediate_bob.0),
325 intermediate_bob_y: Some(intermediate_bob.1),
326 };
327
328 let processor = NoteProcessor::new(&key_repo, compliance_pk);
329 let wallet_note = processor
330 .process(&event)
331 .expect("should decrypt transfer memo");
332 assert_eq!(wallet_note.note.asset_id, note.asset_id);
333 assert_eq!(wallet_note.note.value, note.value);
334 assert!(wallet_note.is_transfer);
335 assert!(!wallet_note.spent);
336 }
337
338 #[test]
339 fn test_path_b_unknown_tag_returns_none() {
340 let account = DarkAccount::from_seed(b"test_path_b_unknown");
341 let (_, compliance_pk) = create_compliance_keypair();
342 let key_repo = KeyRepository::new(account, compliance_pk);
343
344 let event = UnprocessedEvent {
345 event_type: EventType::NewMemo,
346 block_number: 200,
347 tx_hash: "0x5678".to_string(),
348 leaf_index: 10,
349 commitment: U256::from(12345),
350 epk_x: U256::zero(),
351 epk_y: U256::zero(),
352 packed_ciphertext: [U256::zero(); 7],
353 tag: Some(U256::from(99999)), intermediate_bob_x: Some(U256::from(111)),
355 intermediate_bob_y: Some(U256::from(222)),
356 };
357
358 let processor = NoteProcessor::new(&key_repo, compliance_pk);
359 let result = processor.process(&event);
360
361 assert!(result.is_none(), "Should not match unknown tag");
362 }
363
364 #[test]
365 fn test_memo_missing_fields_returns_none() {
366 let account = DarkAccount::from_seed(b"test_memo_missing");
367 let (_, compliance_pk) = create_compliance_keypair();
368 let key_repo = KeyRepository::new(account, compliance_pk);
369
370 let event1 = UnprocessedEvent {
372 event_type: EventType::NewMemo,
373 block_number: 200,
374 tx_hash: "0x5678".to_string(),
375 leaf_index: 10,
376 commitment: U256::from(12345),
377 epk_x: U256::zero(),
378 epk_y: U256::zero(),
379 packed_ciphertext: [U256::zero(); 7],
380 tag: None, intermediate_bob_x: Some(U256::from(111)),
382 intermediate_bob_y: Some(U256::from(222)),
383 };
384
385 let processor = NoteProcessor::new(&key_repo, compliance_pk);
386 assert!(processor.process(&event1).is_none());
387
388 let event2 = UnprocessedEvent {
390 event_type: EventType::NewMemo,
391 block_number: 200,
392 tx_hash: "0x5678".to_string(),
393 leaf_index: 10,
394 commitment: U256::from(12345),
395 epk_x: U256::zero(),
396 epk_y: U256::zero(),
397 packed_ciphertext: [U256::zero(); 7],
398 tag: Some(U256::from(12345)),
399 intermediate_bob_x: None, intermediate_bob_y: Some(U256::from(222)),
401 };
402
403 assert!(processor.process(&event2).is_none());
404 }
405
406 #[test]
407 fn test_nullifier_path_a_deterministic() {
408 let account = DarkAccount::from_seed(b"test_nullifier_a");
409 let (_, compliance_pk) = create_compliance_keypair();
410 let mut key_repo = KeyRepository::new(account, compliance_pk);
411
412 let (ephemeral_sk, _nonce) = key_repo.next_ephemeral_params();
414 let note = create_test_note();
415 let (packed_ciphertext, epk) =
416 encrypt_note_for_deposit_aes(ephemeral_sk, compliance_pk, ¬e).unwrap();
417 let commitment = crate::crypto_helpers::poseidon_hash(&packed_ciphertext);
418
419 let event = UnprocessedEvent {
421 event_type: EventType::NewNote,
422 block_number: 100,
423 tx_hash: "0x1234".to_string(),
424 leaf_index: 0,
425 commitment,
426 epk_x: epk.0,
427 epk_y: epk.1,
428 packed_ciphertext,
429 tag: None,
430 intermediate_bob_x: None,
431 intermediate_bob_y: None,
432 };
433
434 let processor = NoteProcessor::new(&key_repo, compliance_pk);
435 let result1 = processor.process(&event).unwrap();
436 let result2 = processor.process(&event).unwrap();
437
438 assert_eq!(result1.nullifier, result2.nullifier);
440 let expected_nullifier = derive_nullifier_path_a(note.nullifier);
442 assert_eq!(result1.nullifier, expected_nullifier);
443 }
444}