1use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams};
3use byteorder::{LittleEndian, WriteBytesExt};
4use ff::PrimeField;
5use group::{cofactor::CofactorGroup, GroupEncoding};
6use ironfish_jubjub::{AffinePoint, ExtendedPoint};
7use rand_core::RngCore;
8use std::convert::TryInto;
9
10use zcash_note_encryption::{
11 try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ock,
12 try_output_recovery_with_ovk, BatchDomain, Domain, EphemeralKeyBytes, NoteEncryption,
13 NotePlaintextBytes, OutPlaintextBytes, OutgoingCipherKey, ShieldedOutput, COMPACT_NOTE_SIZE,
14 ENC_CIPHERTEXT_SIZE, NOTE_PLAINTEXT_SIZE, OUT_PLAINTEXT_SIZE,
15};
16
17use crate::{
18 consensus::{self, BlockHeight, NetworkUpgrade::Canopy, ZIP212_GRACE_PERIOD},
19 keys::OutgoingViewingKey,
20 memo::MemoBytes,
21 sapling::{Diversifier, Note, PaymentAddress, Rseed, SaplingIvk},
22 transaction::components::{
23 amount::Amount,
24 sapling::{self, OutputDescription},
25 },
26};
27
28pub const KDF_SAPLING_PERSONALIZATION: &[u8; 16] = b"Zcash_SaplingKDF";
29pub const PRF_OCK_PERSONALIZATION: &[u8; 16] = b"Zcash_Derive_ock";
30
31pub fn sapling_ka_agree(esk: &ironfish_jubjub::Fr, pk_d: &ironfish_jubjub::ExtendedPoint) -> ironfish_jubjub::SubgroupPoint {
35 let mut wnaf = group::Wnaf::new();
40 wnaf.scalar(esk).base(*pk_d).clear_cofactor()
41}
42
43fn kdf_sapling(dhsecret: ironfish_jubjub::SubgroupPoint, ephemeral_key: &EphemeralKeyBytes) -> Blake2bHash {
47 Blake2bParams::new()
48 .hash_length(32)
49 .personal(KDF_SAPLING_PERSONALIZATION)
50 .to_state()
51 .update(&dhsecret.to_bytes())
52 .update(ephemeral_key.as_ref())
53 .finalize()
54}
55
56pub fn prf_ock(
60 ovk: &OutgoingViewingKey,
61 cv: &ironfish_jubjub::ExtendedPoint,
62 cmu_bytes: &[u8; 32],
63 ephemeral_key: &EphemeralKeyBytes,
64) -> OutgoingCipherKey {
65 OutgoingCipherKey(
66 Blake2bParams::new()
67 .hash_length(32)
68 .personal(PRF_OCK_PERSONALIZATION)
69 .to_state()
70 .update(&ovk.0)
71 .update(&cv.to_bytes())
72 .update(cmu_bytes)
73 .update(ephemeral_key.as_ref())
74 .finalize()
75 .as_bytes()
76 .try_into()
77 .unwrap(),
78 )
79}
80
81fn epk_bytes(epk: &ironfish_jubjub::ExtendedPoint) -> EphemeralKeyBytes {
82 EphemeralKeyBytes(epk.to_bytes())
83}
84
85fn sapling_parse_note_plaintext_without_memo<F, P: consensus::Parameters>(
86 domain: &SaplingDomain<P>,
87 plaintext: &[u8],
88 get_validated_pk_d: F,
89) -> Option<(Note, PaymentAddress)>
90where
91 F: FnOnce(&Diversifier) -> Option<ironfish_jubjub::SubgroupPoint>,
92{
93 assert!(plaintext.len() >= COMPACT_NOTE_SIZE);
94
95 if !plaintext_version_is_valid(&domain.params, domain.height, plaintext[0]) {
97 return None;
98 }
99
100 let diversifier = Diversifier(plaintext[1..12].try_into().unwrap());
102 let value = Amount::from_u64_le_bytes(plaintext[12..20].try_into().unwrap()).ok()?;
103 let r: [u8; 32] = plaintext[20..COMPACT_NOTE_SIZE].try_into().unwrap();
104
105 let rseed = if plaintext[0] == 0x01 {
106 let rcm = Option::from(ironfish_jubjub::Fr::from_repr(r))?;
107 Rseed::BeforeZip212(rcm)
108 } else {
109 Rseed::AfterZip212(r)
110 };
111
112 let pk_d = get_validated_pk_d(&diversifier)?;
113
114 let to = PaymentAddress::from_parts(diversifier, pk_d)?;
115 let note = to.create_note(value.into(), rseed)?;
116 Some((note, to))
117}
118
119pub struct SaplingDomain<P: consensus::Parameters> {
120 params: P,
121 height: BlockHeight,
122}
123
124impl<P: consensus::Parameters> SaplingDomain<P> {
125 pub fn for_height(params: P, height: BlockHeight) -> Self {
126 Self { params, height }
127 }
128}
129
130pub struct SaplingExtractedCommitmentBytes(pub [u8; 32]);
131
132impl From<blstrs::Scalar> for SaplingExtractedCommitmentBytes {
133 fn from(value: blstrs::Scalar) -> SaplingExtractedCommitmentBytes {
134 SaplingExtractedCommitmentBytes(value.to_bytes_le())
135 }
136}
137
138impl<'a> From<&'a blstrs::Scalar> for SaplingExtractedCommitmentBytes {
139 fn from(value: &'a blstrs::Scalar) -> SaplingExtractedCommitmentBytes {
140 SaplingExtractedCommitmentBytes(value.to_bytes_le())
141 }
142}
143
144impl PartialEq for SaplingExtractedCommitmentBytes {
145 fn eq(&self, other: &Self) -> bool {
146 self.0 == other.0
147 }
148}
149impl Eq for SaplingExtractedCommitmentBytes {}
150
151impl<P: consensus::Parameters> Domain for SaplingDomain<P> {
152 type EphemeralSecretKey = ironfish_jubjub::Scalar;
153 type EphemeralPublicKey = ironfish_jubjub::ExtendedPoint;
157 type SharedSecret = ironfish_jubjub::SubgroupPoint;
158 type SymmetricKey = Blake2bHash;
159 type Note = Note;
160 type Recipient = PaymentAddress;
161 type DiversifiedTransmissionKey = ironfish_jubjub::SubgroupPoint;
162 type IncomingViewingKey = SaplingIvk;
163 type OutgoingViewingKey = OutgoingViewingKey;
164 type ValueCommitment = ironfish_jubjub::ExtendedPoint;
165 type ExtractedCommitment = blstrs::Scalar;
166 type ExtractedCommitmentBytes = SaplingExtractedCommitmentBytes;
167 type Memo = MemoBytes;
168
169 fn derive_esk(note: &Self::Note) -> Option<Self::EphemeralSecretKey> {
170 note.derive_esk()
171 }
172
173 fn get_pk_d(note: &Self::Note) -> Self::DiversifiedTransmissionKey {
174 note.pk_d
175 }
176
177 fn ka_derive_public(
178 note: &Self::Note,
179 esk: &Self::EphemeralSecretKey,
180 ) -> Self::EphemeralPublicKey {
181 (note.g_d * esk).into()
187 }
188
189 fn ka_agree_enc(
190 esk: &Self::EphemeralSecretKey,
191 pk_d: &Self::DiversifiedTransmissionKey,
192 ) -> Self::SharedSecret {
193 sapling_ka_agree(esk, pk_d.into())
194 }
195
196 fn ka_agree_dec(
197 ivk: &Self::IncomingViewingKey,
198 epk: &Self::EphemeralPublicKey,
199 ) -> Self::SharedSecret {
200 sapling_ka_agree(&ivk.0, epk)
201 }
202
203 fn kdf(dhsecret: ironfish_jubjub::SubgroupPoint, epk: &EphemeralKeyBytes) -> Blake2bHash {
207 kdf_sapling(dhsecret, epk)
208 }
209
210 fn note_plaintext_bytes(
211 note: &Self::Note,
212 to: &Self::Recipient,
213 memo: &Self::Memo,
214 ) -> NotePlaintextBytes {
215 let mut input = [0; NOTE_PLAINTEXT_SIZE];
218 input[0] = match note.rseed {
219 Rseed::BeforeZip212(_) => 1,
220 Rseed::AfterZip212(_) => 2,
221 };
222 input[1..12].copy_from_slice(&to.diversifier().0);
223 (&mut input[12..20])
224 .write_u64::<LittleEndian>(note.value)
225 .unwrap();
226
227 match note.rseed {
228 Rseed::BeforeZip212(rcm) => {
229 input[20..COMPACT_NOTE_SIZE].copy_from_slice(rcm.to_repr().as_ref());
230 }
231 Rseed::AfterZip212(rseed) => {
232 input[20..COMPACT_NOTE_SIZE].copy_from_slice(&rseed);
233 }
234 }
235
236 input[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE].copy_from_slice(&memo.as_array()[..]);
237
238 NotePlaintextBytes(input)
239 }
240
241 fn derive_ock(
242 ovk: &Self::OutgoingViewingKey,
243 cv: &Self::ValueCommitment,
244 cmu_bytes: &Self::ExtractedCommitmentBytes,
245 epk: &EphemeralKeyBytes,
246 ) -> OutgoingCipherKey {
247 prf_ock(ovk, cv, &cmu_bytes.0, epk)
248 }
249
250 fn outgoing_plaintext_bytes(
251 note: &Self::Note,
252 esk: &Self::EphemeralSecretKey,
253 ) -> OutPlaintextBytes {
254 let mut input = [0u8; OUT_PLAINTEXT_SIZE];
255 input[0..32].copy_from_slice(¬e.pk_d.to_bytes());
256 input[32..OUT_PLAINTEXT_SIZE].copy_from_slice(esk.to_repr().as_ref());
257
258 OutPlaintextBytes(input)
259 }
260
261 fn epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes {
262 epk_bytes(epk)
263 }
264
265 fn epk(ephemeral_key: &EphemeralKeyBytes) -> Option<Self::EphemeralPublicKey> {
266 ironfish_jubjub::ExtendedPoint::from_bytes(&ephemeral_key.0).into()
270 }
271
272 fn parse_note_plaintext_without_memo_ivk(
273 &self,
274 ivk: &Self::IncomingViewingKey,
275 plaintext: &[u8],
276 ) -> Option<(Self::Note, Self::Recipient)> {
277 sapling_parse_note_plaintext_without_memo(self, plaintext, |diversifier| {
278 Some(diversifier.g_d()? * ivk.0)
279 })
280 }
281
282 fn parse_note_plaintext_without_memo_ovk(
283 &self,
284 pk_d: &Self::DiversifiedTransmissionKey,
285 esk: &Self::EphemeralSecretKey,
286 ephemeral_key: &EphemeralKeyBytes,
287 plaintext: &NotePlaintextBytes,
288 ) -> Option<(Self::Note, Self::Recipient)> {
289 sapling_parse_note_plaintext_without_memo(self, &plaintext.0, |diversifier| {
290 if (diversifier.g_d()? * esk).to_bytes() == ephemeral_key.0 {
291 Some(*pk_d)
292 } else {
293 None
294 }
295 })
296 }
297
298 fn cmstar(note: &Self::Note) -> Self::ExtractedCommitment {
299 note.cmu()
300 }
301
302 fn extract_pk_d(op: &OutPlaintextBytes) -> Option<Self::DiversifiedTransmissionKey> {
303 ironfish_jubjub::SubgroupPoint::from_bytes(
304 op.0[0..32].try_into().expect("slice is the correct length"),
305 )
306 .into()
307 }
308
309 fn extract_esk(op: &OutPlaintextBytes) -> Option<Self::EphemeralSecretKey> {
310 ironfish_jubjub::Fr::from_repr(
311 op.0[32..OUT_PLAINTEXT_SIZE]
312 .try_into()
313 .expect("slice is the correct length"),
314 )
315 .into()
316 }
317
318 fn extract_memo(&self, plaintext: &NotePlaintextBytes) -> Self::Memo {
319 MemoBytes::from_bytes(&plaintext.0[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE]).unwrap()
320 }
321}
322
323impl<P: consensus::Parameters> BatchDomain for SaplingDomain<P> {
324 fn batch_kdf<'a>(
325 items: impl Iterator<Item = (Option<Self::SharedSecret>, &'a EphemeralKeyBytes)>,
326 ) -> Vec<Option<Self::SymmetricKey>> {
327 let (shared_secrets, ephemeral_keys): (Vec<_>, Vec<_>) = items.unzip();
328
329 let secrets: Vec<_> = shared_secrets
330 .iter()
331 .filter_map(|s| s.map(ExtendedPoint::from))
332 .collect();
333 let mut secrets_affine = vec![AffinePoint::identity(); shared_secrets.len()];
334 group::Curve::batch_normalize(&secrets, &mut secrets_affine);
335
336 let mut secrets_affine = secrets_affine.into_iter();
337 shared_secrets
338 .into_iter()
339 .map(|s| s.and_then(|_| secrets_affine.next()))
340 .zip(ephemeral_keys.into_iter())
341 .map(|(secret, ephemeral_key)| {
342 secret.map(|dhsecret| {
343 Blake2bParams::new()
344 .hash_length(32)
345 .personal(KDF_SAPLING_PERSONALIZATION)
346 .to_state()
347 .update(&dhsecret.to_bytes())
348 .update(ephemeral_key.as_ref())
349 .finalize()
350 })
351 })
352 .collect()
353 }
354
355 fn batch_epk(
356 ephemeral_keys: impl Iterator<Item = EphemeralKeyBytes>,
357 ) -> Vec<(Option<Self::EphemeralPublicKey>, EphemeralKeyBytes)> {
358 let ephemeral_keys: Vec<_> = ephemeral_keys.collect();
359 let epks = ironfish_jubjub::AffinePoint::batch_from_bytes(ephemeral_keys.iter().map(|b| b.0));
360 epks.into_iter()
361 .zip(ephemeral_keys.into_iter())
362 .map(|(epk, ephemeral_key)| {
363 (epk.map(ironfish_jubjub::ExtendedPoint::from).into(), ephemeral_key)
364 })
365 .collect()
366 }
367}
368
369pub fn sapling_note_encryption<R: RngCore, P: consensus::Parameters>(
374 ovk: Option<OutgoingViewingKey>,
375 note: Note,
376 to: PaymentAddress,
377 memo: MemoBytes,
378 rng: &mut R,
379) -> NoteEncryption<SaplingDomain<P>> {
380 let esk = note.generate_or_derive_esk_internal(rng);
381 NoteEncryption::new_with_esk(esk, ovk, note, to, memo)
382}
383
384#[allow(clippy::if_same_then_else)]
385#[allow(clippy::needless_bool)]
386pub fn plaintext_version_is_valid<P: consensus::Parameters>(
387 params: &P,
388 height: BlockHeight,
389 leadbyte: u8,
390) -> bool {
391 if params.is_nu_active(Canopy, height) {
392 let grace_period_end_height =
393 params.activation_height(Canopy).unwrap() + ZIP212_GRACE_PERIOD;
394
395 if height < grace_period_end_height && leadbyte != 0x01 && leadbyte != 0x02 {
396 false
398 } else if height >= grace_period_end_height && leadbyte != 0x02 {
399 false
401 } else {
402 true
403 }
404 } else {
405 leadbyte == 0x01
407 }
408}
409
410pub fn try_sapling_note_decryption<
411 P: consensus::Parameters,
412 Output: ShieldedOutput<SaplingDomain<P>, ENC_CIPHERTEXT_SIZE>,
413>(
414 params: &P,
415 height: BlockHeight,
416 ivk: &SaplingIvk,
417 output: &Output,
418) -> Option<(Note, PaymentAddress, MemoBytes)> {
419 let domain = SaplingDomain {
420 params: params.clone(),
421 height,
422 };
423 try_note_decryption(&domain, ivk, output)
424}
425
426pub fn try_sapling_compact_note_decryption<
427 P: consensus::Parameters,
428 Output: ShieldedOutput<SaplingDomain<P>, COMPACT_NOTE_SIZE>,
429>(
430 params: &P,
431 height: BlockHeight,
432 ivk: &SaplingIvk,
433 output: &Output,
434) -> Option<(Note, PaymentAddress)> {
435 let domain = SaplingDomain {
436 params: params.clone(),
437 height,
438 };
439
440 try_compact_note_decryption(&domain, ivk, output)
441}
442
443pub fn try_sapling_output_recovery_with_ock<P: consensus::Parameters>(
452 params: &P,
453 height: BlockHeight,
454 ock: &OutgoingCipherKey,
455 output: &OutputDescription<sapling::GrothProofBytes>,
456) -> Option<(Note, PaymentAddress, MemoBytes)> {
457 let domain = SaplingDomain {
458 params: params.clone(),
459 height,
460 };
461
462 try_output_recovery_with_ock(&domain, ock, output, &output.out_ciphertext)
463}
464
465#[allow(clippy::too_many_arguments)]
473pub fn try_sapling_output_recovery<P: consensus::Parameters>(
474 params: &P,
475 height: BlockHeight,
476 ovk: &OutgoingViewingKey,
477 output: &OutputDescription<sapling::GrothProofBytes>,
478) -> Option<(Note, PaymentAddress, MemoBytes)> {
479 let domain = SaplingDomain {
480 params: params.clone(),
481 height,
482 };
483
484 try_output_recovery_with_ovk(&domain, ovk, output, &output.cv, &output.out_ciphertext)
485}
486
487#[cfg(test)]
488mod tests {
489 use chacha20poly1305::{
490 aead::{AeadInPlace, NewAead},
491 ChaCha20Poly1305,
492 };
493 use ff::{Field, PrimeField};
494 use group::Group;
495 use group::{cofactor::CofactorGroup, GroupEncoding};
496 use rand_core::OsRng;
497 use rand_core::{CryptoRng, RngCore};
498 use std::convert::TryInto;
499
500 use zcash_note_encryption::{
501 batch, EphemeralKeyBytes, NoteEncryption, OutgoingCipherKey, ENC_CIPHERTEXT_SIZE,
502 NOTE_PLAINTEXT_SIZE, OUT_CIPHERTEXT_SIZE, OUT_PLAINTEXT_SIZE,
503 };
504
505 use super::{
506 epk_bytes, kdf_sapling, prf_ock, sapling_ka_agree, sapling_note_encryption,
507 try_sapling_compact_note_decryption, try_sapling_note_decryption,
508 try_sapling_output_recovery, try_sapling_output_recovery_with_ock, SaplingDomain,
509 };
510
511 use crate::{
512 consensus::{
513 BlockHeight,
514 NetworkUpgrade::{Canopy, Sapling},
515 Parameters, TestNetwork, TEST_NETWORK, ZIP212_GRACE_PERIOD,
516 },
517 keys::OutgoingViewingKey,
518 memo::MemoBytes,
519 sapling::util::generate_random_rseed,
520 sapling::{Diversifier, PaymentAddress, Rseed, SaplingIvk, ValueCommitment},
521 transaction::components::{
522 amount::Amount,
523 sapling::{self, CompactOutputDescription, OutputDescription},
524 GROTH_PROOF_SIZE,
525 },
526 };
527
528 fn random_enc_ciphertext<R: RngCore + CryptoRng>(
529 height: BlockHeight,
530 mut rng: &mut R,
531 ) -> (
532 OutgoingViewingKey,
533 OutgoingCipherKey,
534 SaplingIvk,
535 OutputDescription<sapling::GrothProofBytes>,
536 ) {
537 let ivk = SaplingIvk(ironfish_jubjub::Fr::random(&mut rng));
538
539 let (ovk, ock, output) = random_enc_ciphertext_with(height, &ivk, rng);
540
541 assert!(try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output).is_some());
542 assert!(try_sapling_compact_note_decryption(
543 &TEST_NETWORK,
544 height,
545 &ivk,
546 &CompactOutputDescription::from(output.clone()),
547 )
548 .is_some());
549
550 let ovk_output_recovery = try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output);
551
552 let ock_output_recovery =
553 try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output);
554 assert!(ovk_output_recovery.is_some());
555 assert!(ock_output_recovery.is_some());
556 assert_eq!(ovk_output_recovery, ock_output_recovery);
557
558 (ovk, ock, ivk, output)
559 }
560
561 fn random_enc_ciphertext_with<R: RngCore + CryptoRng>(
562 height: BlockHeight,
563 ivk: &SaplingIvk,
564 mut rng: &mut R,
565 ) -> (
566 OutgoingViewingKey,
567 OutgoingCipherKey,
568 OutputDescription<sapling::GrothProofBytes>,
569 ) {
570 let diversifier = Diversifier([0; 11]);
571 let pk_d = diversifier.g_d().unwrap() * ivk.0;
572 let pa = PaymentAddress::from_parts_unchecked(diversifier, pk_d);
573
574 let value = Amount::from_u64(100).unwrap();
576 let value_commitment = ValueCommitment {
577 value: value.into(),
578 randomness: ironfish_jubjub::Fr::random(&mut rng),
579 };
580 let cv = value_commitment.commitment().into();
581
582 let rseed = generate_random_rseed(&TEST_NETWORK, height, &mut rng);
583
584 let note = pa.create_note(value.into(), rseed).unwrap();
585 let cmu = note.cmu();
586
587 let ovk = OutgoingViewingKey([0; 32]);
588 let ne = sapling_note_encryption::<_, TestNetwork>(
589 Some(ovk),
590 note,
591 pa,
592 MemoBytes::empty(),
593 &mut rng,
594 );
595 let epk = *ne.epk();
596 let ock = prf_ock(&ovk, &cv, &cmu.to_repr(), &epk_bytes(&epk));
597
598 let output = OutputDescription {
599 cv,
600 cmu,
601 ephemeral_key: epk.to_bytes().into(),
602 enc_ciphertext: ne.encrypt_note_plaintext(),
603 out_ciphertext: ne.encrypt_outgoing_plaintext(&cv, &cmu, &mut rng),
604 zkproof: [0u8; GROTH_PROOF_SIZE],
605 };
606
607 (ovk, ock, output)
608 }
609
610 fn reencrypt_enc_ciphertext(
611 ovk: &OutgoingViewingKey,
612 cv: &ironfish_jubjub::ExtendedPoint,
613 cmu: &blstrs::Scalar,
614 ephemeral_key: &EphemeralKeyBytes,
615 enc_ciphertext: &mut [u8; ENC_CIPHERTEXT_SIZE],
616 out_ciphertext: &[u8; OUT_CIPHERTEXT_SIZE],
617 modify_plaintext: impl Fn(&mut [u8; NOTE_PLAINTEXT_SIZE]),
618 ) {
619 let ock = prf_ock(ovk, cv, &cmu.to_repr(), ephemeral_key);
620
621 let mut op = [0; OUT_PLAINTEXT_SIZE];
622 op.copy_from_slice(&out_ciphertext[..OUT_PLAINTEXT_SIZE]);
623
624 ChaCha20Poly1305::new(ock.as_ref().into())
625 .decrypt_in_place_detached(
626 [0u8; 12][..].into(),
627 &[],
628 &mut op,
629 out_ciphertext[OUT_PLAINTEXT_SIZE..].into(),
630 )
631 .unwrap();
632
633 let pk_d = ironfish_jubjub::SubgroupPoint::from_bytes(&op[0..32].try_into().unwrap()).unwrap();
634
635 let esk = ironfish_jubjub::Fr::from_repr(op[32..OUT_PLAINTEXT_SIZE].try_into().unwrap()).unwrap();
636
637 let shared_secret = sapling_ka_agree(&esk, &pk_d.into());
638 let key = kdf_sapling(shared_secret, ephemeral_key);
639
640 let mut plaintext = [0; NOTE_PLAINTEXT_SIZE];
641 plaintext.copy_from_slice(&enc_ciphertext[..NOTE_PLAINTEXT_SIZE]);
642
643 ChaCha20Poly1305::new(key.as_bytes().into())
644 .decrypt_in_place_detached(
645 [0u8; 12][..].into(),
646 &[],
647 &mut plaintext,
648 enc_ciphertext[NOTE_PLAINTEXT_SIZE..].into(),
649 )
650 .unwrap();
651
652 modify_plaintext(&mut plaintext);
653
654 let tag = ChaCha20Poly1305::new(key.as_ref().into())
655 .encrypt_in_place_detached([0u8; 12][..].into(), &[], &mut plaintext)
656 .unwrap();
657
658 enc_ciphertext[..NOTE_PLAINTEXT_SIZE].copy_from_slice(&plaintext);
659 enc_ciphertext[NOTE_PLAINTEXT_SIZE..].copy_from_slice(&tag);
660 }
661
662 fn find_invalid_diversifier() -> Diversifier {
663 let mut d = Diversifier([0; 11]);
665 loop {
666 for k in 0..11 {
667 d.0[k] = d.0[k].wrapping_add(1);
668 if d.0[k] != 0 {
669 break;
670 }
671 }
672 if d.g_d().is_none() {
673 break;
674 }
675 }
676 d
677 }
678
679 fn find_valid_diversifier() -> Diversifier {
680 let mut d = Diversifier([0; 11]);
682 loop {
683 for k in 0..11 {
684 d.0[k] = d.0[k].wrapping_add(1);
685 if d.0[k] != 0 {
686 break;
687 }
688 }
689 if d.g_d().is_some() {
690 break;
691 }
692 }
693 d
694 }
695
696 #[test]
697 fn decryption_with_invalid_ivk() {
698 let mut rng = OsRng;
699 let heights = [
700 TEST_NETWORK.activation_height(Sapling).unwrap(),
701 TEST_NETWORK.activation_height(Canopy).unwrap(),
702 ];
703
704 for &height in heights.iter() {
705 let (_, _, _, output) = random_enc_ciphertext(height, &mut rng);
706
707 assert_eq!(
708 try_sapling_note_decryption(
709 &TEST_NETWORK,
710 height,
711 &SaplingIvk(ironfish_jubjub::Fr::random(&mut rng)),
712 &output
713 ),
714 None
715 );
716 }
717 }
718
719 #[test]
720 fn decryption_with_invalid_epk() {
721 let mut rng = OsRng;
722 let heights = [
723 TEST_NETWORK.activation_height(Sapling).unwrap(),
724 TEST_NETWORK.activation_height(Canopy).unwrap(),
725 ];
726
727 for &height in heights.iter() {
728 let (_, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng);
729
730 output.ephemeral_key = ironfish_jubjub::ExtendedPoint::random(&mut rng).to_bytes().into();
731
732 assert_eq!(
733 try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output,),
734 None
735 );
736 }
737 }
738
739 #[test]
740 fn decryption_with_invalid_cmu() {
741 let mut rng = OsRng;
742 let heights = [
743 TEST_NETWORK.activation_height(Sapling).unwrap(),
744 TEST_NETWORK.activation_height(Canopy).unwrap(),
745 ];
746
747 for &height in heights.iter() {
748 let (_, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng);
749 output.cmu = blstrs::Scalar::random(&mut rng);
750
751 assert_eq!(
752 try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output),
753 None
754 );
755 }
756 }
757
758 #[test]
759 fn decryption_with_invalid_tag() {
760 let mut rng = OsRng;
761 let heights = [
762 TEST_NETWORK.activation_height(Sapling).unwrap(),
763 TEST_NETWORK.activation_height(Canopy).unwrap(),
764 ];
765
766 for &height in heights.iter() {
767 let (_, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng);
768 output.enc_ciphertext[ENC_CIPHERTEXT_SIZE - 1] ^= 0xff;
769
770 assert_eq!(
771 try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output),
772 None
773 );
774 }
775 }
776
777 #[test]
778 fn decryption_with_invalid_version_byte() {
779 let mut rng = OsRng;
780 let canopy_activation_height = TEST_NETWORK.activation_height(Canopy).unwrap();
781 let heights = [
782 canopy_activation_height - 1,
783 canopy_activation_height,
784 canopy_activation_height + ZIP212_GRACE_PERIOD,
785 ];
786 let leadbytes = [0x02, 0x03, 0x01];
787
788 for (&height, &leadbyte) in heights.iter().zip(leadbytes.iter()) {
789 let (ovk, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng);
790
791 reencrypt_enc_ciphertext(
792 &ovk,
793 &output.cv,
794 &output.cmu,
795 &output.ephemeral_key,
796 &mut output.enc_ciphertext,
797 &output.out_ciphertext,
798 |pt| pt[0] = leadbyte,
799 );
800 assert_eq!(
801 try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output),
802 None
803 );
804 }
805 }
806
807 #[test]
808 fn decryption_with_invalid_diversifier() {
809 let mut rng = OsRng;
810 let heights = [
811 TEST_NETWORK.activation_height(Sapling).unwrap(),
812 TEST_NETWORK.activation_height(Canopy).unwrap(),
813 ];
814
815 for &height in heights.iter() {
816 let (ovk, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng);
817
818 reencrypt_enc_ciphertext(
819 &ovk,
820 &output.cv,
821 &output.cmu,
822 &output.ephemeral_key,
823 &mut output.enc_ciphertext,
824 &output.out_ciphertext,
825 |pt| pt[1..12].copy_from_slice(&find_invalid_diversifier().0),
826 );
827 assert_eq!(
828 try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output),
829 None
830 );
831 }
832 }
833
834 #[test]
835 fn decryption_with_incorrect_diversifier() {
836 let mut rng = OsRng;
837 let heights = [
838 TEST_NETWORK.activation_height(Sapling).unwrap(),
839 TEST_NETWORK.activation_height(Canopy).unwrap(),
840 ];
841
842 for &height in heights.iter() {
843 let (ovk, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng);
844
845 reencrypt_enc_ciphertext(
846 &ovk,
847 &output.cv,
848 &output.cmu,
849 &output.ephemeral_key,
850 &mut output.enc_ciphertext,
851 &output.out_ciphertext,
852 |pt| pt[1..12].copy_from_slice(&find_valid_diversifier().0),
853 );
854
855 assert_eq!(
856 try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output),
857 None
858 );
859 }
860 }
861
862 #[test]
863 fn compact_decryption_with_invalid_ivk() {
864 let mut rng = OsRng;
865 let heights = [
866 TEST_NETWORK.activation_height(Sapling).unwrap(),
867 TEST_NETWORK.activation_height(Canopy).unwrap(),
868 ];
869
870 for &height in heights.iter() {
871 let (_, _, _, output) = random_enc_ciphertext(height, &mut rng);
872
873 assert_eq!(
874 try_sapling_compact_note_decryption(
875 &TEST_NETWORK,
876 height,
877 &SaplingIvk(ironfish_jubjub::Fr::random(&mut rng)),
878 &CompactOutputDescription::from(output)
879 ),
880 None
881 );
882 }
883 }
884
885 #[test]
886 fn compact_decryption_with_invalid_epk() {
887 let mut rng = OsRng;
888 let heights = [
889 TEST_NETWORK.activation_height(Sapling).unwrap(),
890 TEST_NETWORK.activation_height(Canopy).unwrap(),
891 ];
892
893 for &height in heights.iter() {
894 let (_, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng);
895 output.ephemeral_key = ironfish_jubjub::ExtendedPoint::random(&mut rng).to_bytes().into();
896
897 assert_eq!(
898 try_sapling_compact_note_decryption(
899 &TEST_NETWORK,
900 height,
901 &ivk,
902 &CompactOutputDescription::from(output)
903 ),
904 None
905 );
906 }
907 }
908
909 #[test]
910 fn compact_decryption_with_invalid_cmu() {
911 let mut rng = OsRng;
912 let heights = [
913 TEST_NETWORK.activation_height(Sapling).unwrap(),
914 TEST_NETWORK.activation_height(Canopy).unwrap(),
915 ];
916
917 for &height in heights.iter() {
918 let (_, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng);
919 output.cmu = blstrs::Scalar::random(&mut rng);
920
921 assert_eq!(
922 try_sapling_compact_note_decryption(
923 &TEST_NETWORK,
924 height,
925 &ivk,
926 &CompactOutputDescription::from(output)
927 ),
928 None
929 );
930 }
931 }
932
933 #[test]
934 fn compact_decryption_with_invalid_version_byte() {
935 let mut rng = OsRng;
936 let canopy_activation_height = TEST_NETWORK.activation_height(Canopy).unwrap();
937 let heights = [
938 canopy_activation_height - 1,
939 canopy_activation_height,
940 canopy_activation_height + ZIP212_GRACE_PERIOD,
941 ];
942 let leadbytes = [0x02, 0x03, 0x01];
943
944 for (&height, &leadbyte) in heights.iter().zip(leadbytes.iter()) {
945 let (ovk, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng);
946
947 reencrypt_enc_ciphertext(
948 &ovk,
949 &output.cv,
950 &output.cmu,
951 &output.ephemeral_key,
952 &mut output.enc_ciphertext,
953 &output.out_ciphertext,
954 |pt| pt[0] = leadbyte,
955 );
956 assert_eq!(
957 try_sapling_compact_note_decryption(
958 &TEST_NETWORK,
959 height,
960 &ivk,
961 &CompactOutputDescription::from(output)
962 ),
963 None
964 );
965 }
966 }
967
968 #[test]
969 fn compact_decryption_with_invalid_diversifier() {
970 let mut rng = OsRng;
971 let heights = [
972 TEST_NETWORK.activation_height(Sapling).unwrap(),
973 TEST_NETWORK.activation_height(Canopy).unwrap(),
974 ];
975
976 for &height in heights.iter() {
977 let (ovk, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng);
978
979 reencrypt_enc_ciphertext(
980 &ovk,
981 &output.cv,
982 &output.cmu,
983 &output.ephemeral_key,
984 &mut output.enc_ciphertext,
985 &output.out_ciphertext,
986 |pt| pt[1..12].copy_from_slice(&find_invalid_diversifier().0),
987 );
988 assert_eq!(
989 try_sapling_compact_note_decryption(
990 &TEST_NETWORK,
991 height,
992 &ivk,
993 &CompactOutputDescription::from(output)
994 ),
995 None
996 );
997 }
998 }
999
1000 #[test]
1001 fn compact_decryption_with_incorrect_diversifier() {
1002 let mut rng = OsRng;
1003 let heights = [
1004 TEST_NETWORK.activation_height(Sapling).unwrap(),
1005 TEST_NETWORK.activation_height(Canopy).unwrap(),
1006 ];
1007
1008 for &height in heights.iter() {
1009 let (ovk, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng);
1010
1011 reencrypt_enc_ciphertext(
1012 &ovk,
1013 &output.cv,
1014 &output.cmu,
1015 &output.ephemeral_key,
1016 &mut output.enc_ciphertext,
1017 &output.out_ciphertext,
1018 |pt| pt[1..12].copy_from_slice(&find_valid_diversifier().0),
1019 );
1020 assert_eq!(
1021 try_sapling_compact_note_decryption(
1022 &TEST_NETWORK,
1023 height,
1024 &ivk,
1025 &CompactOutputDescription::from(output)
1026 ),
1027 None
1028 );
1029 }
1030 }
1031
1032 #[test]
1033 fn recovery_with_invalid_ovk() {
1034 let mut rng = OsRng;
1035 let heights = [
1036 TEST_NETWORK.activation_height(Sapling).unwrap(),
1037 TEST_NETWORK.activation_height(Canopy).unwrap(),
1038 ];
1039
1040 for &height in heights.iter() {
1041 let (mut ovk, _, _, output) = random_enc_ciphertext(height, &mut rng);
1042
1043 ovk.0[0] ^= 0xff;
1044 assert_eq!(
1045 try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,),
1046 None
1047 );
1048 }
1049 }
1050
1051 #[test]
1052 fn recovery_with_invalid_ock() {
1053 let mut rng = OsRng;
1054 let heights = [
1055 TEST_NETWORK.activation_height(Sapling).unwrap(),
1056 TEST_NETWORK.activation_height(Canopy).unwrap(),
1057 ];
1058
1059 for &height in heights.iter() {
1060 let (_, _, _, output) = random_enc_ciphertext(height, &mut rng);
1061
1062 assert_eq!(
1063 try_sapling_output_recovery_with_ock(
1064 &TEST_NETWORK,
1065 height,
1066 &OutgoingCipherKey([0u8; 32]),
1067 &output,
1068 ),
1069 None
1070 );
1071 }
1072 }
1073
1074 #[test]
1075 fn recovery_with_invalid_cv() {
1076 let mut rng = OsRng;
1077 let heights = [
1078 TEST_NETWORK.activation_height(Sapling).unwrap(),
1079 TEST_NETWORK.activation_height(Canopy).unwrap(),
1080 ];
1081
1082 for &height in heights.iter() {
1083 let (ovk, _, _, mut output) = random_enc_ciphertext(height, &mut rng);
1084 output.cv = ironfish_jubjub::ExtendedPoint::random(&mut rng);
1085
1086 assert_eq!(
1087 try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,),
1088 None
1089 );
1090 }
1091 }
1092
1093 #[test]
1094 fn recovery_with_invalid_cmu() {
1095 let mut rng = OsRng;
1096 let heights = [
1097 TEST_NETWORK.activation_height(Sapling).unwrap(),
1098 TEST_NETWORK.activation_height(Canopy).unwrap(),
1099 ];
1100
1101 for &height in heights.iter() {
1102 let (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng);
1103 output.cmu = blstrs::Scalar::random(&mut rng);
1104
1105 assert_eq!(
1106 try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,),
1107 None
1108 );
1109
1110 assert_eq!(
1111 try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output,),
1112 None
1113 );
1114 }
1115 }
1116
1117 #[test]
1118 fn recovery_with_invalid_epk() {
1119 let mut rng = OsRng;
1120 let heights = [
1121 TEST_NETWORK.activation_height(Sapling).unwrap(),
1122 TEST_NETWORK.activation_height(Canopy).unwrap(),
1123 ];
1124
1125 for &height in heights.iter() {
1126 let (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng);
1127 output.ephemeral_key = ironfish_jubjub::ExtendedPoint::random(&mut rng).to_bytes().into();
1128
1129 assert_eq!(
1130 try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,),
1131 None
1132 );
1133
1134 assert_eq!(
1135 try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output,),
1136 None
1137 );
1138 }
1139 }
1140
1141 #[test]
1142 fn recovery_with_invalid_enc_tag() {
1143 let mut rng = OsRng;
1144 let heights = [
1145 TEST_NETWORK.activation_height(Sapling).unwrap(),
1146 TEST_NETWORK.activation_height(Canopy).unwrap(),
1147 ];
1148
1149 for &height in heights.iter() {
1150 let (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng);
1151
1152 output.enc_ciphertext[ENC_CIPHERTEXT_SIZE - 1] ^= 0xff;
1153 assert_eq!(
1154 try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,),
1155 None
1156 );
1157 assert_eq!(
1158 try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output,),
1159 None
1160 );
1161 }
1162 }
1163
1164 #[test]
1165 fn recovery_with_invalid_out_tag() {
1166 let mut rng = OsRng;
1167 let heights = [
1168 TEST_NETWORK.activation_height(Sapling).unwrap(),
1169 TEST_NETWORK.activation_height(Canopy).unwrap(),
1170 ];
1171
1172 for &height in heights.iter() {
1173 let (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng);
1174
1175 output.out_ciphertext[OUT_CIPHERTEXT_SIZE - 1] ^= 0xff;
1176 assert_eq!(
1177 try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,),
1178 None
1179 );
1180 assert_eq!(
1181 try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output,),
1182 None
1183 );
1184 }
1185 }
1186
1187 #[test]
1188 fn recovery_with_invalid_version_byte() {
1189 let mut rng = OsRng;
1190 let canopy_activation_height = TEST_NETWORK.activation_height(Canopy).unwrap();
1191 let heights = [
1192 canopy_activation_height - 1,
1193 canopy_activation_height,
1194 canopy_activation_height + ZIP212_GRACE_PERIOD,
1195 ];
1196 let leadbytes = [0x02, 0x03, 0x01];
1197
1198 for (&height, &leadbyte) in heights.iter().zip(leadbytes.iter()) {
1199 let (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng);
1200
1201 reencrypt_enc_ciphertext(
1202 &ovk,
1203 &output.cv,
1204 &output.cmu,
1205 &output.ephemeral_key,
1206 &mut output.enc_ciphertext,
1207 &output.out_ciphertext,
1208 |pt| pt[0] = leadbyte,
1209 );
1210 assert_eq!(
1211 try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,),
1212 None
1213 );
1214 assert_eq!(
1215 try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output,),
1216 None
1217 );
1218 }
1219 }
1220
1221 #[test]
1222 fn recovery_with_invalid_diversifier() {
1223 let mut rng = OsRng;
1224 let heights = [
1225 TEST_NETWORK.activation_height(Sapling).unwrap(),
1226 TEST_NETWORK.activation_height(Canopy).unwrap(),
1227 ];
1228
1229 for &height in heights.iter() {
1230 let (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng);
1231
1232 reencrypt_enc_ciphertext(
1233 &ovk,
1234 &output.cv,
1235 &output.cmu,
1236 &output.ephemeral_key,
1237 &mut output.enc_ciphertext,
1238 &output.out_ciphertext,
1239 |pt| pt[1..12].copy_from_slice(&find_invalid_diversifier().0),
1240 );
1241 assert_eq!(
1242 try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,),
1243 None
1244 );
1245 assert_eq!(
1246 try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output,),
1247 None
1248 );
1249 }
1250 }
1251
1252 #[test]
1253 fn recovery_with_incorrect_diversifier() {
1254 let mut rng = OsRng;
1255 let heights = [
1256 TEST_NETWORK.activation_height(Sapling).unwrap(),
1257 TEST_NETWORK.activation_height(Canopy).unwrap(),
1258 ];
1259
1260 for &height in heights.iter() {
1261 let (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng);
1262
1263 reencrypt_enc_ciphertext(
1264 &ovk,
1265 &output.cv,
1266 &output.cmu,
1267 &output.ephemeral_key,
1268 &mut output.enc_ciphertext,
1269 &output.out_ciphertext,
1270 |pt| pt[1..12].copy_from_slice(&find_valid_diversifier().0),
1271 );
1272 assert_eq!(
1273 try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,),
1274 None
1275 );
1276 assert_eq!(
1277 try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output,),
1278 None
1279 );
1280 }
1281 }
1282
1283 #[test]
1284 fn recovery_with_invalid_pk_d() {
1285 let mut rng = OsRng;
1286 let heights = [
1287 TEST_NETWORK.activation_height(Sapling).unwrap(),
1288 TEST_NETWORK.activation_height(Canopy).unwrap(),
1289 ];
1290
1291 for &height in heights.iter() {
1292 let ivk = SaplingIvk(ironfish_jubjub::Fr::zero());
1293 let (ovk, ock, output) = random_enc_ciphertext_with(height, &ivk, &mut rng);
1294
1295 assert_eq!(
1296 try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,),
1297 None
1298 );
1299 assert_eq!(
1300 try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output,),
1301 None
1302 );
1303 }
1304 }
1305
1306 #[test]
1307 fn test_vectors() {
1308 let test_vectors = crate::test_vectors::note_encryption::make_test_vectors();
1309
1310 macro_rules! read_blstrs_scalar {
1311 ($field:expr) => {{
1312 blstrs::Scalar::from_repr($field[..].try_into().unwrap()).unwrap()
1313 }};
1314 }
1315
1316 macro_rules! read_jubjub_scalar {
1317 ($field:expr) => {{
1318 ironfish_jubjub::Fr::from_repr($field[..].try_into().unwrap()).unwrap()
1319 }};
1320 }
1321
1322 macro_rules! read_point {
1323 ($field:expr) => {
1324 ironfish_jubjub::ExtendedPoint::from_bytes(&$field).unwrap()
1325 };
1326 }
1327
1328 let height = TEST_NETWORK.activation_height(Sapling).unwrap();
1329
1330 for tv in test_vectors {
1331 let ivk = SaplingIvk(read_jubjub_scalar!(tv.ivk));
1336 let pk_d = read_point!(tv.default_pk_d).into_subgroup().unwrap();
1337 let rcm = read_jubjub_scalar!(tv.rcm);
1338 let cv = read_point!(tv.cv);
1339 let cmu = read_blstrs_scalar!(tv.cmu);
1340 let esk = read_jubjub_scalar!(tv.esk);
1341 let ephemeral_key = EphemeralKeyBytes(tv.epk);
1342
1343 let shared_secret = sapling_ka_agree(&esk, &pk_d.into());
1348 assert_eq!(shared_secret.to_bytes(), tv.shared_secret);
1349
1350 let k_enc = kdf_sapling(shared_secret, &ephemeral_key);
1351 assert_eq!(k_enc.as_bytes(), tv.k_enc);
1352
1353 let ovk = OutgoingViewingKey(tv.ovk);
1354 let ock = prf_ock(&ovk, &cv, &cmu.to_repr(), &ephemeral_key);
1355 assert_eq!(ock.as_ref(), tv.ock);
1356
1357 let to = PaymentAddress::from_parts(Diversifier(tv.default_d), pk_d).unwrap();
1358 let note = to.create_note(tv.v, Rseed::BeforeZip212(rcm)).unwrap();
1359 assert_eq!(note.cmu(), cmu);
1360
1361 let output = OutputDescription {
1362 cv,
1363 cmu,
1364 ephemeral_key,
1365 enc_ciphertext: tv.c_enc,
1366 out_ciphertext: tv.c_out,
1367 zkproof: [0u8; GROTH_PROOF_SIZE],
1368 };
1369
1370 match try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output) {
1376 Some((decrypted_note, decrypted_to, decrypted_memo)) => {
1377 assert_eq!(decrypted_note, note);
1378 assert_eq!(decrypted_to, to);
1379 assert_eq!(&decrypted_memo.as_array()[..], &tv.memo[..]);
1380 }
1381 None => panic!("Note decryption failed"),
1382 }
1383
1384 match try_sapling_compact_note_decryption(
1385 &TEST_NETWORK,
1386 height,
1387 &ivk,
1388 &CompactOutputDescription::from(output.clone()),
1389 ) {
1390 Some((decrypted_note, decrypted_to)) => {
1391 assert_eq!(decrypted_note, note);
1392 assert_eq!(decrypted_to, to);
1393 }
1394 None => panic!("Compact note decryption failed"),
1395 }
1396
1397 match try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output) {
1398 Some((decrypted_note, decrypted_to, decrypted_memo)) => {
1399 assert_eq!(decrypted_note, note);
1400 assert_eq!(decrypted_to, to);
1401 assert_eq!(&decrypted_memo.as_array()[..], &tv.memo[..]);
1402 }
1403 None => panic!("Output recovery failed"),
1404 }
1405
1406 match &batch::try_note_decryption(
1407 &[ivk.clone()],
1408 &[(
1409 SaplingDomain::for_height(TEST_NETWORK, height),
1410 output.clone(),
1411 )],
1412 )[..]
1413 {
1414 [Some((decrypted_note, decrypted_to, decrypted_memo))] => {
1415 assert_eq!(decrypted_note, ¬e);
1416 assert_eq!(decrypted_to, &to);
1417 assert_eq!(&decrypted_memo.as_array()[..], &tv.memo[..]);
1418 }
1419 _ => panic!("Note decryption failed"),
1420 }
1421
1422 match &batch::try_compact_note_decryption(
1423 &[ivk.clone()],
1424 &[(
1425 SaplingDomain::for_height(TEST_NETWORK, height),
1426 CompactOutputDescription::from(output.clone()),
1427 )],
1428 )[..]
1429 {
1430 [Some((decrypted_note, decrypted_to))] => {
1431 assert_eq!(decrypted_note, ¬e);
1432 assert_eq!(decrypted_to, &to);
1433 }
1434 _ => panic!("Note decryption failed"),
1435 }
1436
1437 let ne = NoteEncryption::<SaplingDomain<TestNetwork>>::new_with_esk(
1442 esk,
1443 Some(ovk),
1444 note,
1445 to,
1446 MemoBytes::from_bytes(&tv.memo).unwrap(),
1447 );
1448
1449 assert_eq!(ne.encrypt_note_plaintext().as_ref(), &tv.c_enc[..]);
1450 assert_eq!(
1451 &ne.encrypt_outgoing_plaintext(&cv, &cmu, &mut OsRng)[..],
1452 &tv.c_out[..]
1453 );
1454 }
1455 }
1456
1457 #[test]
1458 fn batching() {
1459 let mut rng = OsRng;
1460 let height = TEST_NETWORK.activation_height(Canopy).unwrap();
1461
1462 let invalid_ivk = SaplingIvk(ironfish_jubjub::Fr::random(rng));
1464 let valid_ivk = SaplingIvk(ironfish_jubjub::Fr::random(rng));
1465 let outputs: Vec<_> = (0..10)
1466 .map(|_| {
1467 (
1468 SaplingDomain::for_height(TEST_NETWORK, height),
1469 random_enc_ciphertext_with(height, &valid_ivk, &mut rng).2,
1470 )
1471 })
1472 .collect();
1473
1474 let res = batch::try_note_decryption(&[invalid_ivk.clone(), valid_ivk.clone()], &outputs);
1475 assert_eq!(res.len(), 20);
1476 assert_eq!(&res[..10], &vec![None; 10][..]);
1478 for (result, (_, output)) in res[10..].iter().zip(outputs.iter()) {
1479 assert_eq!(
1481 try_sapling_note_decryption(&TEST_NETWORK, height, &invalid_ivk, output),
1482 None
1483 );
1484
1485 assert!(result.is_some());
1487 assert_eq!(
1488 result,
1489 &try_sapling_note_decryption(&TEST_NETWORK, height, &valid_ivk, output)
1490 );
1491 }
1492 }
1493}