ironfish_primitives/sapling/
note_encryption.rs

1//! Implementation of in-band secret distribution for Zcash transactions.
2use 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
31/// Sapling key agreement for note encryption.
32///
33/// Implements section 5.4.4.3 of the Zcash Protocol Specification.
34pub fn sapling_ka_agree(esk: &ironfish_jubjub::Fr, pk_d: &ironfish_jubjub::ExtendedPoint) -> ironfish_jubjub::SubgroupPoint {
35    // [8 esk] pk_d
36    // <ExtendedPoint as CofactorGroup>::clear_cofactor is implemented using
37    // ExtendedPoint::mul_by_cofactor in the ironfish_jubjub crate.
38
39    let mut wnaf = group::Wnaf::new();
40    wnaf.scalar(esk).base(*pk_d).clear_cofactor()
41}
42
43/// Sapling KDF for note encryption.
44///
45/// Implements section 5.4.4.4 of the Zcash Protocol Specification.
46fn 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
56/// Sapling PRF^ock.
57///
58/// Implemented per section 5.4.2 of the Zcash Protocol Specification.
59pub 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    // Check note plaintext version
96    if !plaintext_version_is_valid(&domain.params, domain.height, plaintext[0]) {
97        return None;
98    }
99
100    // The unwraps below are guaranteed to succeed by the assertion above
101    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    // It is acceptable for this to be a point because we enforce by consensus that
154    // points must not be small-order, and all points with non-canonical serialization
155    // are small-order.
156    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        // epk is an element of ironfish_jubjub's prime-order subgroup,
182        // but Self::EphemeralPublicKey is a full group element
183        // for efficiency of encryption. The conversion here is fine
184        // because the output of this function is only used for
185        // encoding and the byte encoding is unaffected by the conversion.
186        (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    /// Sapling KDF for note encryption.
204    ///
205    /// Implements section 5.4.4.4 of the Zcash Protocol Specification.
206    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        // Note plaintext encoding is defined in section 5.5 of the Zcash Protocol
216        // Specification.
217        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(&note.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        // ZIP 216: We unconditionally reject non-canonical encodings, because these have
267        // always been rejected by consensus (due to small-order checks).
268        // https://zips.z.cash/zip-0216#specification
269        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
369/// Creates a new encryption context for the given note.
370///
371/// Setting `ovk` to `None` represents the `ovk = ⊥` case, where the note cannot be
372/// recovered by the sender.
373pub 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            // non-{0x01,0x02} received after Canopy activation and before grace period has elapsed
397            false
398        } else if height >= grace_period_end_height && leadbyte != 0x02 {
399            // non-0x02 received past (Canopy activation height + grace period)
400            false
401        } else {
402            true
403        }
404    } else {
405        // return false if non-0x01 received when Canopy is not active
406        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
443/// Recovery of the full note plaintext by the sender.
444///
445/// Attempts to decrypt and validate the given `enc_ciphertext` using the given `ock`.
446/// If successful, the corresponding Sapling note and memo are returned, along with the
447/// `PaymentAddress` to which the note was sent.
448///
449/// Implements part of section 4.19.3 of the Zcash Protocol Specification.
450/// For decryption using a Full Viewing Key see [`try_sapling_output_recovery`].
451pub 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/// Recovery of the full note plaintext by the sender.
466///
467/// Attempts to decrypt and validate the given `enc_ciphertext` using the given `ovk`.
468/// If successful, the corresponding Sapling note and memo are returned, along with the
469/// `PaymentAddress` to which the note was sent.
470///
471/// Implements section 4.19.3 of the Zcash Protocol Specification.
472#[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        // Construct the value commitment for the proof instance
575        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        // Find an invalid diversifier
664        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        // Find a different valid diversifier
681        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            //
1332            // Load the test vector components
1333            //
1334
1335            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            //
1344            // Test the individual components
1345            //
1346
1347            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            //
1371            // Test decryption
1372            // (Tested first because it only requires immutable references.)
1373            //
1374
1375            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, &note);
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, &note);
1432                    assert_eq!(decrypted_to, &to);
1433                }
1434                _ => panic!("Note decryption failed"),
1435            }
1436
1437            //
1438            // Test encryption
1439            //
1440
1441            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        // Test batch trial-decryption with multiple IVKs and outputs.
1463        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        // The batched trial decryptions with invalid_ivk failed.
1477        assert_eq!(&res[..10], &vec![None; 10][..]);
1478        for (result, (_, output)) in res[10..].iter().zip(outputs.iter()) {
1479            // Confirm that the outputs should indeed have failed with invalid_ivk
1480            assert_eq!(
1481                try_sapling_note_decryption(&TEST_NETWORK, height, &invalid_ivk, output),
1482                None
1483            );
1484
1485            // Confirm the successful batched trial decryptions gave the same result.
1486            assert!(result.is_some());
1487            assert_eq!(
1488                result,
1489                &try_sapling_note_decryption(&TEST_NETWORK, height, &valid_ivk, output)
1490            );
1491        }
1492    }
1493}