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::{
6    keys::{ExpandedSpendingKey, FullViewingKey},
7    zip32::ExtendedSpendingKey,
8};
9
10use super::{
11    Diversifier, Nullifier, NullifierDerivingKey, PaymentAddress, keys::EphemeralSecretKey,
12    value::NoteValue,
13};
14
15mod commitment;
16pub use self::commitment::{ExtractedNoteCommitment, NoteCommitment};
17
18pub(super) mod nullifier;
19
20/// Enum for note randomness before and after [ZIP 212](https://zips.z.cash/zip-0212).
21///
22/// Before ZIP 212, the note commitment trapdoor `rcm` must be a scalar value.
23/// After ZIP 212, the note randomness `rseed` is a 32-byte sequence, used to derive
24/// both the note commitment trapdoor `rcm` and the ephemeral private key `esk`.
25#[derive(Copy, Clone, Debug, PartialEq, Eq)]
26pub enum Rseed {
27    BeforeZip212(jubjub::Fr),
28    AfterZip212([u8; 32]),
29}
30
31impl Rseed {
32    /// Defined in [Zcash Protocol Spec § 4.7.2: Sending Notes (Sapling)][saplingsend].
33    ///
34    /// [saplingsend]: https://zips.z.cash/protocol/protocol.pdf#saplingsend
35    pub(crate) fn rcm(&self) -> commitment::NoteCommitTrapdoor {
36        commitment::NoteCommitTrapdoor(match self {
37            Rseed::BeforeZip212(rcm) => *rcm,
38            Rseed::AfterZip212(rseed) => {
39                jubjub::Fr::from_bytes_wide(&PrfExpand::SAPLING_RCM.with(rseed))
40            }
41        })
42    }
43}
44
45/// A discrete amount of funds received by an address.
46#[derive(Clone, Debug)]
47pub struct Note {
48    /// The recipient of the funds.
49    recipient: PaymentAddress,
50    /// The value of this note.
51    value: NoteValue,
52    /// The seed randomness for various note components.
53    rseed: Rseed,
54}
55
56impl PartialEq for Note {
57    fn eq(&self, other: &Self) -> bool {
58        // Notes are canonically defined by their commitments.
59        self.cmu().eq(&other.cmu())
60    }
61}
62
63impl Eq for Note {}
64
65impl Note {
66    /// Creates a note from its component parts.
67    ///
68    /// # Caveats
69    ///
70    /// This low-level constructor enforces that the provided arguments produce an
71    /// internally valid `Note`. However, it allows notes to be constructed in a way that
72    /// violates required security checks for note decryption, as specified in
73    /// [Section 4.19] of the Zcash Protocol Specification. Users of this constructor
74    /// should only call it with note components that have been fully validated by
75    /// decrypting a received note according to [Section 4.19].
76    ///
77    /// [Section 4.19]: https://zips.z.cash/protocol/protocol.pdf#saplingandorchardinband
78    pub fn from_parts(recipient: PaymentAddress, value: NoteValue, rseed: Rseed) -> Self {
79        Note {
80            recipient,
81            value,
82            rseed,
83        }
84    }
85
86    /// Returns the recipient of this note.
87    pub fn recipient(&self) -> PaymentAddress {
88        self.recipient
89    }
90
91    /// Returns the value of this note.
92    pub fn value(&self) -> NoteValue {
93        self.value
94    }
95
96    /// Returns the rseed value of this note.
97    pub fn rseed(&self) -> &Rseed {
98        &self.rseed
99    }
100
101    /// Computes the note commitment, returning the full point.
102    fn cm_full_point(&self) -> NoteCommitment {
103        NoteCommitment::derive(
104            self.recipient.g_d().to_bytes(),
105            self.recipient.pk_d().to_bytes(),
106            self.value,
107            self.rseed.rcm(),
108        )
109    }
110
111    /// Computes the nullifier given the nullifier deriving key and
112    /// note position
113    pub fn nf(&self, nk: &NullifierDerivingKey, position: u64) -> Nullifier {
114        Nullifier::derive(nk, self.cm_full_point(), position)
115    }
116
117    /// Computes the note commitment
118    pub fn cmu(&self) -> ExtractedNoteCommitment {
119        self.cm_full_point().into()
120    }
121
122    /// Defined in [Zcash Protocol Spec § 4.7.2: Sending Notes (Sapling)][saplingsend].
123    ///
124    /// [saplingsend]: https://zips.z.cash/protocol/protocol.pdf#saplingsend
125    pub fn rcm(&self) -> jubjub::Fr {
126        self.rseed.rcm().0
127    }
128
129    /// Derives `esk` from the internal `Rseed` value, or generates a random value if this
130    /// note was created with a v1 (i.e. pre-ZIP 212) note plaintext.
131    pub fn generate_or_derive_esk<R: Rng + CryptoRng>(&self, rng: &mut R) -> EphemeralSecretKey {
132        self.generate_or_derive_esk_internal(rng)
133    }
134
135    pub(crate) fn generate_or_derive_esk_internal<R: Rng>(
136        &self,
137        rng: &mut R,
138    ) -> EphemeralSecretKey {
139        match self.derive_esk() {
140            None => EphemeralSecretKey(jubjub::Fr::random(rng)),
141            Some(esk) => esk,
142        }
143    }
144
145    /// Returns the derived `esk` if this note was created after ZIP 212 activated.
146    pub(crate) fn derive_esk(&self) -> Option<EphemeralSecretKey> {
147        match self.rseed {
148            Rseed::BeforeZip212(_) => None,
149            Rseed::AfterZip212(rseed) => Some(EphemeralSecretKey(jubjub::Fr::from_bytes_wide(
150                &PrfExpand::SAPLING_ESK.with(&rseed),
151            ))),
152        }
153    }
154
155    /// Generates a dummy spent note.
156    ///
157    /// Defined in [Zcash Protocol Spec § 4.8.2: Dummy Notes (Sapling)][saplingdummynotes].
158    ///
159    /// [saplingdummynotes]: https://zips.z.cash/protocol/nu5.pdf#saplingdummynotes
160    pub(crate) fn dummy<R: Rng>(mut rng: R) -> (ExpandedSpendingKey, FullViewingKey, Self) {
161        let mut sk_bytes = [0; 32];
162        rng.fill_bytes(&mut sk_bytes);
163
164        let extsk = ExtendedSpendingKey::master(&sk_bytes[..]);
165        let fvk = extsk.to_diversifiable_full_viewing_key().fvk().clone();
166        // Dummy notes do not need a reproducible ZIP 32 address index.
167        // Retry because not every Sapling diversifier is valid.
168        let ivk = fvk.vk.ivk();
169        let recipient = loop {
170            let mut diversifier = Diversifier(Default::default());
171            rng.fill_bytes(&mut diversifier.0);
172            if let Some(recipient) = ivk.to_payment_address(diversifier) {
173                break recipient;
174            }
175        };
176
177        let mut rseed_bytes = [0; 32];
178        rng.fill_bytes(&mut rseed_bytes);
179        let rseed = Rseed::AfterZip212(rseed_bytes);
180
181        let note = Note::from_parts(recipient, NoteValue::ZERO, rseed);
182
183        (extsk.expsk, fvk, note)
184    }
185}
186
187#[cfg(any(test, feature = "test-dependencies"))]
188pub(super) mod testing {
189    use proptest::{collection::vec, prelude::*};
190
191    use super::{
192        super::{testing::arb_payment_address, value::NoteValue},
193        ExtractedNoteCommitment, Note, Rseed,
194    };
195
196    #[cfg(test)]
197    pub(super) fn arb_dummy_note() -> impl Strategy<
198        Value = (
199            crate::keys::ExpandedSpendingKey,
200            crate::keys::FullViewingKey,
201            Note,
202        ),
203    > {
204        use rand::{SeedableRng, rngs::StdRng};
205
206        prop::array::uniform32(any::<u8>()).prop_map(|seed| Note::dummy(StdRng::from_seed(seed)))
207    }
208
209    prop_compose! {
210        pub fn arb_note(value: NoteValue)(
211            recipient in arb_payment_address(),
212            rseed in prop::array::uniform32(prop::num::u8::ANY).prop_map(Rseed::AfterZip212)
213        ) -> Note {
214            Note {
215                recipient,
216                value,
217                rseed
218            }
219        }
220    }
221
222    prop_compose! {
223        pub(crate) fn arb_cmu()(
224            cmu in vec(any::<u8>(), 64)
225                .prop_map(|v| <[u8;64]>::try_from(v.as_slice()).unwrap())
226                .prop_map(|v| bls12_381::Scalar::from_bytes_wide(&v)),
227        ) -> ExtractedNoteCommitment {
228            ExtractedNoteCommitment(cmu)
229        }
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use proptest::prelude::*;
237
238    proptest! {
239        #[test]
240        fn dummy_note_preserves_spend_inputs((expsk, fvk, note) in testing::arb_dummy_note()) {
241            prop_assert_eq!(fvk.to_bytes(), FullViewingKey::from_expanded_spending_key(&expsk).to_bytes());
242            prop_assert_eq!(note.value(), NoteValue::ZERO);
243            prop_assert!(matches!(note.rseed(), Rseed::AfterZip212(_)));
244            prop_assert_eq!(
245                Some(note.recipient()),
246                fvk.vk.ivk().to_payment_address(*note.recipient().diversifier())
247            );
248        }
249    }
250}