libzeropool_zkbob/native/
cipher.rs

1use crate::{
2    fawkes_crypto::{
3        ff_uint::{Num, seedbox::{SeedboxChaCha20, SeedBox, SeedBoxGen}},
4        borsh::{BorshSerialize, BorshDeserialize},
5        native::ecc::{EdwardsPoint},
6
7    },
8    native::{
9        account::Account,
10        note::Note,
11        params::PoolParams,
12        key::{derive_key_a, derive_key_p_d}
13    },
14    constants::{self, SHARED_SECRETS_HEAPLESS_SIZE, ACCOUNT_HEAPLESS_SIZE, NOTE_HEAPLESS_SIZE}
15};
16
17use sha3::{Digest, Keccak256};
18
19use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce, aead::AeadMutInPlace};
20use chacha20poly1305::aead::{Aead, NewAead};
21use chacha20poly1305::aead::heapless::Vec as HeaplessVec;
22
23/// Wrapper for HeaplessVec (if buffer size is less or equals to N) or Vec otherwise
24enum Buffer<T, const N: usize> {
25    HeapBuffer(Vec<T>),
26    HeaplessBuffer(HeaplessVec<T, N>)
27}
28
29impl<T, const N: usize> Buffer<T, N> {
30    fn as_slice(&self) -> &[T] {
31        match self {
32            Self::HeapBuffer(vec) => vec.as_slice(),
33            Self::HeaplessBuffer(heapless_vec) => heapless_vec.as_slice()
34        }
35    }
36}
37
38fn keccak256(data:&[u8])->[u8;constants::U256_SIZE] {
39    let mut hasher = Keccak256::new();
40    hasher.update(data);
41    let mut res = [0u8;constants::U256_SIZE];
42    res.iter_mut().zip(hasher.finalize().into_iter()).for_each(|(l,r)| *l=r);
43    res
44}
45
46//key stricly assumed to be unique for all messages. Using this function with multiple messages and one key is insecure!
47fn symcipher_encode(key:&[u8], data:&[u8])->Vec<u8> {
48    assert!(key.len()==constants::U256_SIZE);
49    let nonce = Nonce::from_slice(&constants::ENCRYPTION_NONCE);
50    let cipher = ChaCha20Poly1305::new(Key::from_slice(key));
51    cipher.encrypt(nonce, data.as_ref()).unwrap()
52}
53
54/// Decrypts message in place if `ciphertext.len()` is less or equals to N, otherwise allocates memory in heap.
55/// Key stricly assumed to be unique for all messages. Using this function with multiple messages and one key is insecure!
56fn symcipher_decode<const N: usize>(key: &[u8], ciphertext: &[u8]) -> Option<Buffer<u8, N>> {
57    assert!(key.len()==constants::U256_SIZE);
58    let nonce = Nonce::from_slice(&constants::ENCRYPTION_NONCE);
59    let mut cipher = ChaCha20Poly1305::new(Key::from_slice(key));
60
61    if ciphertext.len() <= N {
62        let mut buffer = HeaplessVec::<u8, N>::from_slice(ciphertext).ok()?;
63        cipher.decrypt_in_place(nonce, b"", &mut buffer).ok()?;
64        Some(Buffer::HeaplessBuffer(buffer))
65    } else {
66        let plain = cipher.decrypt(nonce, ciphertext).ok()?;
67        Some(Buffer::HeapBuffer(plain))
68    }
69}
70
71pub fn encrypt<P: PoolParams>(
72    entropy: &[u8],
73    eta:Num<P::Fr>,
74    account: Account<P::Fr>,
75    note: &[Note<P::Fr>],
76    params:&P
77) -> Vec<u8> {
78    let nozero_notes_num = note.len();
79    let nozero_items_num = nozero_notes_num+1;
80
81
82    let mut sb = SeedboxChaCha20::new_with_salt(entropy);
83
84    let account_data = {
85        let mut account_key = [0u8;constants::U256_SIZE];
86        sb.fill_bytes(&mut account_key);
87        let account_ciphertext = symcipher_encode(&account_key, &account.try_to_vec().unwrap());
88        (account_key, account_ciphertext)
89    };
90    
91    
92    let notes_data = note.iter().map(|e|{
93        let a:Num<P::Fs> = sb.gen();
94        let p_d = EdwardsPoint::subgroup_decompress(e.p_d, params.jubjub()).unwrap();
95        let ecdh =  p_d.mul(a, params.jubjub());
96        let key = keccak256(&ecdh.x.try_to_vec().unwrap());
97        let ciphertext = symcipher_encode(&key, &e.try_to_vec().unwrap());
98        let a_pub = derive_key_p_d(e.d.to_num(), a, params); 
99        (a_pub.x, key, ciphertext)
100        
101    }).collect::<Vec<_>>();
102
103    let shared_secret_data = {
104        let a_p_pub = derive_key_a(sb.gen(), params);
105        let ecdh = a_p_pub.mul(eta.to_other_reduced(), params.jubjub());
106        let key = keccak256(&ecdh.x.try_to_vec().unwrap());
107        let text:Vec<u8> = core::iter::once(&account_data.0[..]).chain(notes_data.iter().map(|e| &e.1[..])).collect::<Vec<_>>().concat();
108        let ciphertext = symcipher_encode(&key, &text);
109        (a_p_pub.x, ciphertext)
110    };
111
112    let mut res = vec![];
113
114    (nozero_items_num as u32).serialize(&mut res).unwrap();
115    account.hash(params).serialize(&mut res).unwrap();
116
117    for e in note.iter() {
118        e.hash(params).serialize(&mut res).unwrap();
119    }
120    shared_secret_data.0.serialize(&mut res).unwrap();
121    res.extend(&shared_secret_data.1);
122
123    res.extend(&account_data.1);
124
125    notes_data.iter().for_each(|nd|{
126        nd.0.serialize(&mut res).unwrap();
127        res.extend(&nd.2);
128    });
129
130    res
131}
132
133
134fn buf_take<'a>(memo: &mut &'a[u8], size:usize) -> Option<&'a[u8]> {
135    if memo.len() < size {
136        None
137    } else {
138        let res = &memo[0..size];
139        *memo = &memo[size..];
140        Some(res)
141    }
142}
143
144pub fn decrypt_out<P: PoolParams>(eta:Num<P::Fr>, mut memo:&[u8], params:&P)->Option<(Account<P::Fr>, Vec<Note<P::Fr>>)> {
145    let num_size = constants::num_size_bits::<P::Fr>()/8;
146    let account_size = constants::account_size_bits::<P::Fr>()/8;
147    let note_size = constants::note_size_bits::<P::Fr>()/8;
148
149
150    let nozero_items_num = u32::deserialize(&mut memo).ok()? as usize;
151    if nozero_items_num == 0 {
152        return None;
153    }
154
155    let nozero_notes_num = nozero_items_num - 1;
156    let shared_secret_ciphertext_size = nozero_items_num * constants::U256_SIZE + constants::POLY_1305_TAG_SIZE;
157
158    let account_hash = Num::deserialize(&mut memo).ok()?;
159    let note_hashes = buf_take(&mut memo, nozero_notes_num * num_size)?;
160
161    let shared_secret_text = decrypt_ecdh::<P, SHARED_SECRETS_HEAPLESS_SIZE>(eta, &mut memo, shared_secret_ciphertext_size, params)?;
162
163    let mut shared_secret_text_ptr = shared_secret_text.as_slice();
164
165    let account_key= <[u8;constants::U256_SIZE]>::deserialize(&mut shared_secret_text_ptr).ok()?;
166    let note_key = (0..nozero_notes_num).map(|_| <[u8;constants::U256_SIZE]>::deserialize(&mut shared_secret_text_ptr)).collect::<Result<Vec<_>,_>>().ok()?;
167
168    let account_ciphertext = buf_take(&mut memo, account_size+constants::POLY_1305_TAG_SIZE)?;
169    let account = decrypt_account(&account_key, account_ciphertext, account_hash, params)?;
170
171    let note = (0..nozero_notes_num).map(|i| {
172        buf_take(&mut memo, num_size)?;
173        let note_hash = {
174            let note_hash = &mut &note_hashes[i * num_size..(i + 1) * num_size];
175            Num::deserialize(note_hash).ok()?
176        };
177
178        let ciphertext = buf_take(&mut memo, note_size+constants::POLY_1305_TAG_SIZE)?;
179
180        decrypt_note(&note_key[i], ciphertext, note_hash, params)
181    }).collect::<Option<Vec<_>>>()?;
182    
183    Some((account, note))
184}
185
186fn decrypt_ecdh<P: PoolParams, const N: usize>(eta:Num<P::Fr>, buf:&mut &[u8], size:usize, params:&P) -> Option<Buffer<u8, N>> {
187    let a_p = EdwardsPoint::subgroup_decompress(Num::deserialize(buf).ok()?, params.jubjub())?;
188    let ecdh = a_p.mul(eta.to_other_reduced(), params.jubjub());
189    let key = {
190        let mut x: [u8; 32] = [0; 32];
191        ecdh.x.serialize(&mut &mut x[..]).unwrap();
192        keccak256(&x)
193    };
194    let ciphertext = buf_take(buf, size)?;
195
196    symcipher_decode::<N>(&key, ciphertext)
197}
198
199fn _decrypt_in<P: PoolParams>(eta:Num<P::Fr>, mut memo:&[u8], params:&P)->Option<Vec<Option<Note<P::Fr>>>> {
200    let num_size = constants::num_size_bits::<P::Fr>()/8;
201    let account_size = constants::account_size_bits::<P::Fr>()/8;
202    let note_size = constants::note_size_bits::<P::Fr>()/8;
203
204
205    let nozero_items_num = u32::deserialize(&mut memo).ok()? as usize;
206    if nozero_items_num == 0 {
207        return None;
208    }
209
210    let nozero_notes_num = nozero_items_num - 1;
211    let shared_secret_ciphertext_size = nozero_items_num * constants::U256_SIZE + constants::POLY_1305_TAG_SIZE;
212
213    buf_take(&mut memo, num_size)?;
214    let note_hashes = buf_take(&mut memo, nozero_notes_num * num_size)?;
215
216    buf_take(&mut memo, num_size)?;
217    buf_take(&mut memo, shared_secret_ciphertext_size)?;
218    buf_take(&mut memo, account_size+constants::POLY_1305_TAG_SIZE)?;
219
220
221    let note = (0..nozero_notes_num).map(|i| {
222        let a_pub = EdwardsPoint::subgroup_decompress(Num::deserialize(&mut memo).ok()?, params.jubjub())?;
223        let ecdh = a_pub.mul(eta.to_other_reduced(), params.jubjub());
224        
225        let key = {
226            let mut x: [u8; 32] = [0; 32];
227            ecdh.x.serialize(&mut &mut x[..]).unwrap();
228            keccak256(&x)
229        };
230
231        let note_hash = {
232            let note_hash = &mut &note_hashes[i * num_size..(i + 1) * num_size];
233            Num::deserialize(note_hash).ok()?
234        };
235        
236        let ciphertext = buf_take(&mut memo, note_size+constants::POLY_1305_TAG_SIZE)?;
237
238        decrypt_note(&key, ciphertext, note_hash, params)
239    }).collect::<Vec<Option<_>>>();
240
241    Some(note)
242}
243
244pub fn decrypt_in<P: PoolParams>(eta:Num<P::Fr>, memo:&[u8], params:&P)->Vec<Option<Note<P::Fr>>> {
245    if let Some(res) = _decrypt_in(eta, memo, params) {
246        res
247    } else {
248        vec![]
249    }
250}
251
252/// get encrypted memo chunks with associated decryption keys (chunk: account or note)
253/// returns vector of tupple (index, chunk, key)
254/// indexes are zero-based and enumerated within current memo
255pub fn symcipher_decryption_keys<P: PoolParams>(eta:Num<P::Fr>, mut memo:&[u8], params:&P) -> Option<Vec<(u64, Vec<u8>, Vec<u8>)>> {
256    let num_size = constants::num_size_bits::<P::Fr>()/8;
257    let account_size = constants::account_size_bits::<P::Fr>()/8;
258    let note_size = constants::note_size_bits::<P::Fr>()/8;
259
260    let nozero_items_num = u32::deserialize(&mut memo).ok()? as usize;
261    if nozero_items_num == 0 {
262        return None;
263    }
264
265    let nozero_notes_num = nozero_items_num - 1;
266    let shared_secret_ciphertext_size = nozero_items_num * constants::U256_SIZE + constants::POLY_1305_TAG_SIZE;
267
268    let account_hash = Num::deserialize(&mut memo).ok()?;
269    let note_hashes = buf_take(&mut memo, nozero_notes_num * num_size)?;
270
271    let shared_secret_text = decrypt_ecdh::<P, SHARED_SECRETS_HEAPLESS_SIZE>(eta, &mut memo, shared_secret_ciphertext_size, params);
272
273    if let Some(shared_secret_text) = shared_secret_text {
274        // here is a our transaction, we can restore account and all notes
275        let mut shared_secret_text_ptr = shared_secret_text.as_slice();
276
277        let account_key= <[u8;constants::U256_SIZE]>::deserialize(&mut shared_secret_text_ptr).ok()?;
278        let note_key = (0..nozero_notes_num).map(|_| <[u8;constants::U256_SIZE]>::deserialize(&mut shared_secret_text_ptr)).collect::<Result<Vec<_>,_>>().ok()?;
279
280        let account_ciphertext = buf_take(&mut memo, account_size+constants::POLY_1305_TAG_SIZE)?;
281        let _ = decrypt_account(&account_key, account_ciphertext, account_hash, params)?;
282
283        let account_tuple = (0 as u64, account_ciphertext.to_vec(), account_key.to_vec());
284        let result = Some(account_tuple)
285            .into_iter()
286            .chain(
287                (0..nozero_notes_num).filter_map(|i| {
288                buf_take(&mut memo, num_size)?;
289
290                let note_hash = {
291                    let note_hash = &mut &note_hashes[i * num_size..(i + 1) * num_size];
292                    Num::deserialize(note_hash).ok()?
293                };
294
295                let ciphertext = buf_take(&mut memo, note_size+constants::POLY_1305_TAG_SIZE)?;
296                match decrypt_note(&note_key[i], ciphertext, note_hash, params) {
297                    Some(_) => Some((i as u64 + 1, ciphertext.to_vec(), note_key[i].to_vec())),
298                    _ => None,
299                }
300            })
301        ).collect::<Vec<_>>();
302        
303        Some(result)
304    } else {
305        // search for incoming notes
306        buf_take(&mut memo, account_size+constants::POLY_1305_TAG_SIZE)?;   // skip account
307        let notes = (0..nozero_notes_num).filter_map(|i| {
308            let a_pub = EdwardsPoint::subgroup_decompress(Num::deserialize(&mut memo).ok()?, params.jubjub())?;
309            let ecdh = a_pub.mul(eta.to_other_reduced(), params.jubjub());
310            
311            let key = {
312                let mut x: [u8; 32] = [0; 32];
313                ecdh.x.serialize(&mut &mut x[..]).unwrap();
314                keccak256(&x)
315            };
316    
317            let note_hash = {
318                let note_hash = &mut &note_hashes[i * num_size..(i + 1) * num_size];
319                Num::deserialize(note_hash).ok()?
320            };
321
322            let ciphertext = buf_take(&mut memo, note_size+constants::POLY_1305_TAG_SIZE)?;
323            match decrypt_note(&key, ciphertext, note_hash, params) {
324                Some(_) => Some((i as u64 + 1, ciphertext.to_vec(), key.to_vec())),
325                _ => None,
326            }
327        })
328        .collect();
329
330        Some(notes)
331    }
332}
333
334pub fn decrypt_account<P: PoolParams>(symkey: &[u8], ciphertext: &[u8], hash: Num<P::Fr>, params: &P) -> Option<Account<P::Fr>> {
335    match decrypt_account_no_validate(symkey, ciphertext, params) {
336        Some(acc) if acc.hash(params) == hash => Some(acc),
337        _ => None,
338    }
339}
340
341pub fn decrypt_account_no_validate<P: PoolParams>(symkey: &[u8], ciphertext: &[u8], _: &P) -> Option<Account<P::Fr>> {
342    let plain = symcipher_decode::<ACCOUNT_HEAPLESS_SIZE>(&symkey, ciphertext)?;
343    Account::try_from_slice(plain.as_slice()).ok()
344}
345
346pub fn decrypt_note<P: PoolParams>(symkey: &[u8], ciphertext: &[u8], hash: Num<P::Fr>, params: &P) -> Option<Note<P::Fr>> {
347    match decrypt_note_no_validate(symkey, ciphertext, params) {
348        Some(note) if note.hash(params) == hash => Some(note),
349        _ => None,
350    }
351}
352
353pub fn decrypt_note_no_validate<P: PoolParams>(symkey: &[u8], ciphertext: &[u8], _: &P) -> Option<Note<P::Fr>> {
354    let plain = symcipher_decode::<NOTE_HEAPLESS_SIZE>(&symkey, ciphertext)?;
355    Note::try_from_slice(plain.as_slice()).ok()
356}
357
358#[cfg(test)]
359mod tests {
360    use test_case::test_case;
361
362    use crate::native::cipher::{symcipher_decryption_keys, decrypt_account, decrypt_note};
363    use crate::native::note::Note;
364    use crate::{POOL_PARAMS, native::boundednum::BoundedNum};
365    use crate::native::account::Account;
366    use fawkes_crypto::ff_uint::Num;
367    use fawkes_crypto::{rand::Rng, engines::bn256::Fr};
368    use fawkes_crypto::rand::rngs::OsRng;
369    use crate::native::key::{derive_key_a, derive_key_eta, derive_key_p_d};
370
371    use super::{symcipher_encode, symcipher_decode, encrypt, decrypt_out, decrypt_in};
372
373    #[test_case(0)]
374    #[test_case(1)]
375    #[test_case(100)]
376    #[test_case(128)]
377    #[test_case(1024)]
378    fn test_symcipher(buf_len: usize) {
379        let mut rng = OsRng::default();
380
381        let key: [u8; 32] = rng.gen();
382        let plaintext: Vec<u8> = (0..buf_len).map(|_| { rng.gen() }).collect();
383        let ciphertext = symcipher_encode(&key, &plaintext.as_slice());
384        let decrypted = symcipher_decode::<0>(&key, &ciphertext.as_slice()).unwrap();
385
386        assert_eq!(plaintext.len(), decrypted.as_slice().len());
387        assert_eq!(plaintext.as_slice(), decrypted.as_slice());
388
389    }
390
391    #[test_case(0, 0.0)]
392    #[test_case(1, 0.0)]
393    #[test_case(1, 1.0)]
394    #[test_case(5, 0.8)]
395    #[test_case(15, 0.0)]
396    #[test_case(15, 1.0)]
397    #[test_case(20, 0.5)]
398    #[test_case(30, 0.7)]
399    #[test_case(42, 0.5)]
400    fn test_decrypt_in_out(notes_count: u32, note_probability: f64) {
401        let params = &POOL_PARAMS.clone();
402        let mut rng = OsRng::default();
403
404        // sender eta
405        let eta1 = derive_key_eta(derive_key_a(rng.gen(), params).x, params);
406        // receciver eta
407        let eta2 = derive_key_eta(derive_key_a(rng.gen(), params).x, params);
408
409        // output account
410        let mut account: Account<Fr> = Account::sample(&mut rng, params);
411        account.b = BoundedNum::new(Num::from(10000000000 as u64));
412        account.e = BoundedNum::new(Num::from(12345 as u64));
413        account.i = BoundedNum::new(Num::from(128 as u32));
414        account.p_d = derive_key_p_d(account.d.to_num(), eta1, params).x;
415
416        // output notes
417        let mut dst_notes_num: usize = 0;
418        let notes: Vec<Note<Fr>> = (0..notes_count as u64).map(|_| {
419            let mut a_note = Note::sample(&mut rng, params);
420            a_note.b = BoundedNum::new(Num::from(500000000 as u64));
421            if rng.gen_bool(note_probability) {
422                // a few notes to the receiver
423                a_note.p_d = derive_key_p_d(a_note.d.to_num(), eta2, params).x;
424                dst_notes_num += 1;
425            } else {
426                // other notes are loopback
427                a_note.p_d = derive_key_p_d(a_note.d.to_num(), eta1, params).x;
428            }
429            a_note
430        }).collect();
431
432        // encrypt account and notes with the sender key
433        let entropy: [u8; 32] = rng.gen();
434        let mut encrypted = encrypt(&entropy, eta1, account, notes.as_slice(), params);
435
436        // let's decrypt the memo from the receiver side and check the result
437        let decrypted_in = decrypt_in(eta2, encrypted.as_mut_slice(), params);
438        assert_eq!(decrypted_in.len(), notes.len());
439        let in_notes: Vec<_> = decrypted_in
440                .into_iter()
441                .enumerate()
442                .filter_map(|(i, note)| {
443                    match note {
444                        Some(note) => { //if note.p_d == key::derive_key_p_d(note.d.to_num(), *eta, params).x => {
445                            assert_eq!(&note, notes.get(i).unwrap());
446                            Some(note)
447                        }
448                        _ => None,
449                    }
450                })
451                .collect();
452        assert_eq!(in_notes.len(), dst_notes_num);
453        
454        // decrypt the memo from the sender side and check the result
455        let decrypted_out = decrypt_out(eta1, encrypted.as_mut_slice(), params);
456        let decrypted_acc = decrypted_out.as_ref().unwrap().0;
457        let decrypted_notes = &decrypted_out.as_ref().unwrap().1;
458        assert_eq!(decrypted_acc, account);
459        assert_eq!(decrypted_notes.len(), notes.len());
460        (0..notes.len()).for_each(|i: usize| {
461            let src = notes.get(i).unwrap();
462            let recovered = decrypted_notes.get(i).unwrap();
463            assert_eq!(src, recovered);
464        });
465    }
466
467    #[test_case(0, 0.0)]
468    #[test_case(1, 0.0)]
469    #[test_case(1, 1.0)]
470    #[test_case(3, 0.5)]
471    #[test_case(10, 0.5)]
472    #[test_case(15, 0.0)]
473    #[test_case(30, 1.0)]
474    #[test_case(42, 0.5)]
475    fn test_compliance(notes_count: u32, note_probability: f64) {
476        let params = &POOL_PARAMS.clone();
477        let mut rng = OsRng::default();
478
479        // sender eta
480        let eta1 = derive_key_eta(derive_key_a(rng.gen(), params).x, params);
481        // receciver eta
482        let eta2 = derive_key_eta(derive_key_a(rng.gen(), params).x, params);
483        // third-party eta
484        let eta3 = derive_key_eta(derive_key_a(rng.gen(), params).x, params);
485
486        // output account
487        let mut account: Account<Fr> = Account::sample(&mut rng, params);
488        account.b = BoundedNum::new(Num::from(10000000000 as u64));
489        account.e = BoundedNum::new(Num::from(12345 as u64));
490        account.i = BoundedNum::new(Num::from(128 as u32));
491        account.p_d = derive_key_p_d(account.d.to_num(), eta1, params).x;
492
493        // output notes
494        let mut dst_notes_num: usize = 0;
495        let notes: Vec<Note<Fr>> = (0..notes_count as u64).map(|_| {
496            let mut a_note = Note::sample(&mut rng, params);
497            a_note.b = BoundedNum::new(Num::from(500000000 as u64));
498            if rng.gen_bool(note_probability) {
499                // a few notes to the receiver
500                a_note.p_d = derive_key_p_d(a_note.d.to_num(), eta2, params).x;
501                dst_notes_num += 1;
502            } else {
503                // other notes are loopback
504                a_note.p_d = derive_key_p_d(a_note.d.to_num(), eta1, params).x;
505            }
506            a_note
507        }).collect();
508
509        // encrypt account and notes with the sender key
510        let entropy: [u8; 32] = rng.gen();
511        let encrypted = encrypt(&entropy, eta1, account, notes.as_slice(), params);
512
513        // trying to restore chunks and associated decryption keys from the sender side
514        let sender_restored = symcipher_decryption_keys(eta1, encrypted.as_slice(), params).unwrap();
515        assert!(sender_restored.len() == notes.len() + 1);
516        sender_restored.iter().for_each(|(index, chunk, key)| {
517            if *index == 0 {
518                // decrypt account
519                let decrypt_acc = decrypt_account(key.as_slice(), chunk.as_slice(), account.hash(params), params).unwrap();
520                assert_eq!(decrypt_acc, account);
521            } else {
522                // decrypt note
523                let orig_note = notes.get((index - 1) as usize).unwrap();
524                let decrypt_note = decrypt_note(key.as_slice(), chunk.as_slice(), orig_note.hash(params), params).unwrap();
525                assert_eq!(decrypt_note, *orig_note);
526            }
527        });
528
529        // trying to restore chunks and associated decryption keys from the receiver side
530        let receiver_restored = symcipher_decryption_keys(eta2, encrypted.as_slice(), params).unwrap();
531        assert!(receiver_restored.len() == dst_notes_num);
532        receiver_restored.iter().for_each(|(index, chunk, key)| {
533            assert_ne!(*index, 0); // account shouldn't be decrypted on receiver side
534            // decrypt note
535            let orig_note = notes.get((index - 1) as usize).unwrap();
536            let decrypt_note = decrypt_note(key.as_slice(), chunk.as_slice(), orig_note.hash(params), params).unwrap();
537            assert_eq!(decrypt_note, *orig_note);
538        });
539
540        // trying to restore memo from the third-party actor
541        let thirdparty_restored = symcipher_decryption_keys(eta3, encrypted.as_slice(), params).unwrap();
542        assert_eq!(thirdparty_restored.len(), 0);
543    }
544}