Skip to main content

sapling_crypto/
note.rs

1use group::{GroupEncoding, ff::Field};
2use rand_core::{CryptoRng, Rng};
3use zcash_spec::PrfExpand;
4
5use crate::keys::{ExpandedSpendingKey, FullViewingKey};
6
7use super::{
8    Diversifier, Nullifier, NullifierDerivingKey, PaymentAddress, keys::EphemeralSecretKey,
9    value::NoteValue,
10};
11
12mod commitment;
13pub use self::commitment::{ExtractedNoteCommitment, NoteCommitment};
14
15pub(super) mod nullifier;
16
17/// Enum for note randomness before and after [ZIP 212](https://zips.z.cash/zip-0212).
18///
19/// Before ZIP 212, the note commitment trapdoor `rcm` must be a scalar value.
20/// After ZIP 212, the note randomness `rseed` is a 32-byte sequence, used to derive
21/// both the note commitment trapdoor `rcm` and the ephemeral private key `esk`.
22#[derive(Copy, Clone, Debug, PartialEq, Eq)]
23pub enum Rseed {
24    BeforeZip212(jubjub::Fr),
25    AfterZip212([u8; 32]),
26}
27
28impl Rseed {
29    /// Defined in [Zcash Protocol Spec § 4.7.2: Sending Notes (Sapling)][saplingsend].
30    ///
31    /// [saplingsend]: https://zips.z.cash/protocol/protocol.pdf#saplingsend
32    pub(crate) fn rcm(&self) -> commitment::NoteCommitTrapdoor {
33        commitment::NoteCommitTrapdoor(match self {
34            Rseed::BeforeZip212(rcm) => *rcm,
35            Rseed::AfterZip212(rseed) => {
36                jubjub::Fr::from_bytes_wide(&PrfExpand::SAPLING_RCM.with(rseed))
37            }
38        })
39    }
40}
41
42/// A discrete amount of funds received by an address.
43#[derive(Clone, Debug)]
44pub struct Note {
45    /// The recipient of the funds.
46    recipient: PaymentAddress,
47    /// The value of this note.
48    value: NoteValue,
49    /// The seed randomness for various note components.
50    rseed: Rseed,
51}
52
53impl PartialEq for Note {
54    fn eq(&self, other: &Self) -> bool {
55        // Notes are canonically defined by their commitments.
56        self.cmu().eq(&other.cmu())
57    }
58}
59
60impl Eq for Note {}
61
62impl Note {
63    /// Creates a note from its component parts.
64    ///
65    /// # Caveats
66    ///
67    /// This low-level constructor enforces that the provided arguments produce an
68    /// internally valid `Note`. However, it allows notes to be constructed in a way that
69    /// violates required security checks for note decryption, as specified in
70    /// [Section 4.19] of the Zcash Protocol Specification. Users of this constructor
71    /// should only call it with note components that have been fully validated by
72    /// decrypting a received note according to [Section 4.19].
73    ///
74    /// [Section 4.19]: https://zips.z.cash/protocol/protocol.pdf#saplingandorchardinband
75    pub fn from_parts(recipient: PaymentAddress, value: NoteValue, rseed: Rseed) -> Self {
76        Note {
77            recipient,
78            value,
79            rseed,
80        }
81    }
82
83    /// Returns the recipient of this note.
84    pub fn recipient(&self) -> PaymentAddress {
85        self.recipient
86    }
87
88    /// Returns the value of this note.
89    pub fn value(&self) -> NoteValue {
90        self.value
91    }
92
93    /// Returns the rseed value of this note.
94    pub fn rseed(&self) -> &Rseed {
95        &self.rseed
96    }
97
98    /// Computes the note commitment, returning the full point.
99    fn cm_full_point(&self) -> NoteCommitment {
100        NoteCommitment::derive(
101            self.recipient.g_d().to_bytes(),
102            self.recipient.pk_d().to_bytes(),
103            self.value,
104            self.rseed.rcm(),
105        )
106    }
107
108    /// Computes the nullifier given the nullifier deriving key and
109    /// note position
110    pub fn nf(&self, nk: &NullifierDerivingKey, position: u64) -> Nullifier {
111        Nullifier::derive(nk, self.cm_full_point(), position)
112    }
113
114    /// Computes the note commitment
115    pub fn cmu(&self) -> ExtractedNoteCommitment {
116        self.cm_full_point().into()
117    }
118
119    /// Defined in [Zcash Protocol Spec § 4.7.2: Sending Notes (Sapling)][saplingsend].
120    ///
121    /// [saplingsend]: https://zips.z.cash/protocol/protocol.pdf#saplingsend
122    pub fn rcm(&self) -> jubjub::Fr {
123        self.rseed.rcm().0
124    }
125
126    /// Derives `esk` from the internal `Rseed` value, or generates a random value if this
127    /// note was created with a v1 (i.e. pre-ZIP 212) note plaintext.
128    pub fn generate_or_derive_esk<R: Rng + CryptoRng>(&self, rng: &mut R) -> EphemeralSecretKey {
129        self.generate_or_derive_esk_internal(rng)
130    }
131
132    pub(crate) fn generate_or_derive_esk_internal<R: Rng>(
133        &self,
134        rng: &mut R,
135    ) -> EphemeralSecretKey {
136        match self.derive_esk() {
137            None => EphemeralSecretKey(jubjub::Fr::random(rng)),
138            Some(esk) => esk,
139        }
140    }
141
142    /// Returns the derived `esk` if this note was created after ZIP 212 activated.
143    pub(crate) fn derive_esk(&self) -> Option<EphemeralSecretKey> {
144        match self.rseed {
145            Rseed::BeforeZip212(_) => None,
146            Rseed::AfterZip212(rseed) => Some(EphemeralSecretKey(jubjub::Fr::from_bytes_wide(
147                &PrfExpand::SAPLING_ESK.with(&rseed),
148            ))),
149        }
150    }
151
152    /// Generates a dummy spent note.
153    ///
154    /// Defined in [Zcash Protocol Spec § 4.8.2: Dummy Notes (Sapling)][saplingdummynotes].
155    ///
156    /// [saplingdummynotes]: https://zips.z.cash/protocol/nu5.pdf#saplingdummynotes
157    pub(crate) fn dummy<R: Rng>(mut rng: R) -> (ExpandedSpendingKey, FullViewingKey, Self) {
158        let mut sk_bytes = [0; 32];
159        rng.fill_bytes(&mut sk_bytes);
160
161        let expanded = crate::keys::master_key_expansion(&sk_bytes);
162        let expsk = ExpandedSpendingKey::from_spending_key(&expanded.as_bytes()[..32]);
163        let fvk = FullViewingKey::from_expanded_spending_key(&expsk);
164        // Dummy notes do not need a reproducible ZIP 32 address index.
165        // Retry because not every Sapling diversifier is valid.
166        let ivk = fvk.vk.ivk();
167        let recipient = loop {
168            let mut diversifier = Diversifier(Default::default());
169            rng.fill_bytes(&mut diversifier.0);
170            if let Some(recipient) = ivk.to_payment_address(diversifier) {
171                break recipient;
172            }
173        };
174
175        let mut rseed_bytes = [0; 32];
176        rng.fill_bytes(&mut rseed_bytes);
177        let rseed = Rseed::AfterZip212(rseed_bytes);
178
179        let note = Note::from_parts(recipient, NoteValue::ZERO, rseed);
180
181        (expsk, fvk, note)
182    }
183}
184
185#[cfg(any(test, feature = "test-dependencies"))]
186pub(super) mod testing {
187    use proptest::{collection::vec, prelude::*};
188
189    use super::{
190        super::{testing::arb_payment_address, value::NoteValue},
191        ExtractedNoteCommitment, Note, Rseed,
192    };
193
194    #[cfg(test)]
195    pub(super) fn arb_dummy_note() -> impl Strategy<
196        Value = (
197            crate::keys::ExpandedSpendingKey,
198            crate::keys::FullViewingKey,
199            Note,
200        ),
201    > {
202        use rand::{SeedableRng, rngs::StdRng};
203
204        prop::array::uniform32(any::<u8>()).prop_map(|seed| Note::dummy(StdRng::from_seed(seed)))
205    }
206
207    prop_compose! {
208        pub fn arb_note(value: NoteValue)(
209            recipient in arb_payment_address(),
210            rseed in prop::array::uniform32(prop::num::u8::ANY).prop_map(Rseed::AfterZip212)
211        ) -> Note {
212            Note {
213                recipient,
214                value,
215                rseed
216            }
217        }
218    }
219
220    prop_compose! {
221        pub(crate) fn arb_cmu()(
222            cmu in vec(any::<u8>(), 64)
223                .prop_map(|v| <[u8;64]>::try_from(v.as_slice()).unwrap())
224                .prop_map(|v| bls12_381::Scalar::from_bytes_wide(&v)),
225        ) -> ExtractedNoteCommitment {
226            ExtractedNoteCommitment(cmu)
227        }
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use proptest::prelude::*;
235    use rand_core::Rng;
236
237    struct CountingRng {
238        inner: rand::rngs::StdRng,
239        consumed: usize,
240    }
241
242    impl CountingRng {
243        fn new(seed: [u8; 32]) -> Self {
244            use rand::SeedableRng;
245            Self {
246                inner: rand::rngs::StdRng::from_seed(seed),
247                consumed: 0,
248            }
249        }
250    }
251
252    impl rand_core::TryRng for CountingRng {
253        type Error = core::convert::Infallible;
254
255        fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
256            self.consumed += 4;
257            Ok(self.inner.next_u32())
258        }
259
260        fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
261            self.consumed += 8;
262            Ok(self.inner.next_u64())
263        }
264
265        fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
266            self.consumed += dest.len();
267            self.inner.fill_bytes(dest);
268            Ok(())
269        }
270    }
271
272    #[test]
273    fn dummy_note_derivation_and_randomness_are_unchanged() {
274        // Pin the key components, note contents, byte consumption, and following RNG word.
275        for (seed, expected_digest, expected_consumed, expected_next) in [
276            (
277                0,
278                [
279                    41, 218, 228, 136, 208, 212, 88, 195, 115, 92, 245, 135, 95, 110, 148, 169,
280                    158, 88, 190, 39, 10, 7, 195, 164, 90, 163, 1, 153, 243, 52, 195, 120,
281                ],
282                75,
283                13252731160361427084,
284            ),
285            (
286                1,
287                [
288                    80, 222, 41, 137, 135, 161, 238, 87, 159, 97, 83, 44, 55, 26, 90, 235, 73, 242,
289                    19, 253, 241, 193, 97, 107, 128, 181, 30, 4, 36, 54, 219, 95,
290                ],
291                97,
292                5790603339235468134,
293            ),
294            (
295                90,
296                [
297                    37, 56, 194, 138, 186, 67, 253, 3, 44, 106, 220, 35, 50, 167, 23, 229, 103,
298                    254, 35, 78, 246, 202, 154, 49, 81, 75, 110, 47, 145, 89, 215, 103,
299                ],
300                86,
301                1661224483263062408,
302            ),
303            (
304                255,
305                [
306                    129, 220, 116, 71, 84, 161, 163, 78, 90, 212, 167, 228, 217, 73, 167, 88, 28,
307                    140, 94, 132, 29, 33, 79, 149, 187, 226, 153, 216, 31, 70, 156, 85,
308                ],
309                97,
310                4758269131877494279,
311            ),
312        ] {
313            let mut rng = CountingRng::new([seed; 32]);
314            let (expsk, fvk, note) = Note::dummy(&mut rng);
315            let mut hash = blake2b_simd::Params::new().hash_length(32).to_state();
316            hash.update(&expsk.to_bytes());
317            hash.update(&fvk.to_bytes());
318            hash.update(&note.recipient().to_bytes());
319            hash.update(&note.cmu().to_bytes());
320            match note.rseed() {
321                Rseed::AfterZip212(seed) => {
322                    hash.update(seed);
323                }
324                _ => panic!("dummy notes use ZIP 212 randomness"),
325            }
326            let consumed = rng.consumed;
327            let next = rng.next_u64();
328            assert_eq!(hash.finalize().as_bytes(), expected_digest);
329            assert_eq!(consumed, expected_consumed);
330            assert_eq!(next, expected_next);
331        }
332    }
333
334    proptest! {
335        #[test]
336        fn dummy_note_preserves_spend_inputs((expsk, fvk, note) in testing::arb_dummy_note()) {
337            prop_assert_eq!(fvk.to_bytes(), FullViewingKey::from_expanded_spending_key(&expsk).to_bytes());
338            prop_assert_eq!(note.value(), NoteValue::ZERO);
339            prop_assert!(matches!(note.rseed(), Rseed::AfterZip212(_)));
340            prop_assert_eq!(
341                Some(note.recipient()),
342                fvk.vk.ivk().to_payment_address(*note.recipient().diversifier())
343            );
344        }
345    }
346}