zebra_chain/sapling/
commitment.rs

1//! Note and value commitments.
2
3use std::{fmt, io};
4
5use bitvec::prelude::*;
6use jubjub::ExtendedPoint;
7use lazy_static::lazy_static;
8use rand_core::{CryptoRng, RngCore};
9
10use crate::{
11    amount::{Amount, NonNegative},
12    error::{NoteCommitmentError, RandError},
13    serialization::{
14        serde_helpers, ReadZcashExt, SerializationError, ZcashDeserialize, ZcashSerialize,
15    },
16};
17
18use super::keys::{find_group_hash, Diversifier, TransmissionKey};
19
20pub mod pedersen_hashes;
21
22#[cfg(test)]
23mod test_vectors;
24
25use pedersen_hashes::*;
26
27/// Generates a random scalar from the scalar field 𝔽_{r_𝕁}.
28///
29/// The prime order subgroup 𝕁^(r) is the order-r_𝕁 subgroup of 𝕁 that consists
30/// of the points whose order divides r. This function is useful when generating
31/// the uniform distribution on 𝔽_{r_𝕁} needed for Sapling commitment schemes'
32/// trapdoor generators.
33///
34/// <https://zips.z.cash/protocol/protocol.pdf#jubjub>
35pub fn generate_trapdoor<T>(csprng: &mut T) -> Result<jubjub::Fr, RandError>
36where
37    T: RngCore + CryptoRng,
38{
39    let mut bytes = [0u8; 64];
40    csprng
41        .try_fill_bytes(&mut bytes)
42        .map_err(|_| RandError::FillBytes)?;
43    // Fr::from_bytes_wide() reduces the input modulo r via Fr::from_u512()
44    Ok(jubjub::Fr::from_bytes_wide(&bytes))
45}
46
47/// The randomness used in the Pedersen Hash for note commitment.
48#[derive(Copy, Clone, Debug, PartialEq, Eq)]
49pub struct CommitmentRandomness(jubjub::Fr);
50
51/// Note commitments for the output notes.
52#[derive(Clone, Copy, Deserialize, PartialEq, Eq, Serialize)]
53pub struct NoteCommitment(#[serde(with = "serde_helpers::AffinePoint")] pub jubjub::AffinePoint);
54
55impl fmt::Debug for NoteCommitment {
56    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
57        f.debug_struct("NoteCommitment")
58            .field("u", &hex::encode(self.0.get_u().to_bytes()))
59            .field("v", &hex::encode(self.0.get_v().to_bytes()))
60            .finish()
61    }
62}
63
64impl From<jubjub::ExtendedPoint> for NoteCommitment {
65    fn from(extended_point: jubjub::ExtendedPoint) -> Self {
66        Self(jubjub::AffinePoint::from(extended_point))
67    }
68}
69
70impl From<NoteCommitment> for [u8; 32] {
71    fn from(cm: NoteCommitment) -> [u8; 32] {
72        cm.0.to_bytes()
73    }
74}
75
76impl TryFrom<[u8; 32]> for NoteCommitment {
77    type Error = &'static str;
78
79    fn try_from(bytes: [u8; 32]) -> Result<Self, Self::Error> {
80        let possible_point = jubjub::AffinePoint::from_bytes(bytes);
81
82        if possible_point.is_some().into() {
83            Ok(Self(possible_point.unwrap()))
84        } else {
85            Err("Invalid jubjub::AffinePoint value")
86        }
87    }
88}
89
90impl NoteCommitment {
91    /// Generate a new _NoteCommitment_ and the randomness used to create it.
92    ///
93    /// We return the randomness because it is needed to construct a _Note_,
94    /// before it is encrypted as part of an _Output Description_. This is a
95    /// higher level function that calls `NoteCommit^Sapling_rcm` internally.
96    ///
97    /// NoteCommit^Sapling_rcm (g*_d , pk*_d , v) :=
98    ///   WindowedPedersenCommit_rcm([1; 6] || I2LEBSP_64(v) || g*_d || pk*_d)
99    ///
100    /// <https://zips.z.cash/protocol/protocol.pdf#concretewindowedcommit>
101    #[allow(non_snake_case)]
102    pub fn new<T>(
103        csprng: &mut T,
104        diversifier: Diversifier,
105        transmission_key: TransmissionKey,
106        value: Amount<NonNegative>,
107    ) -> Result<(CommitmentRandomness, Self), NoteCommitmentError>
108    where
109        T: RngCore + CryptoRng,
110    {
111        // s as in the argument name for WindowedPedersenCommit_r(s)
112        let mut s: BitVec<u8, Lsb0> = BitVec::new();
113
114        // Prefix
115        s.append(&mut bitvec![1; 6]);
116
117        // Jubjub repr_J canonical byte encoding
118        // https://zips.z.cash/protocol/protocol.pdf#jubjub
119        //
120        // The `TryFrom<Diversifier>` impls for the `jubjub::*Point`s handles
121        // calling `DiversifyHash` implicitly.
122
123        let g_d_bytes = jubjub::AffinePoint::try_from(diversifier)
124            .map_err(|_| NoteCommitmentError::InvalidDiversifier)?
125            .to_bytes();
126
127        let pk_d_bytes = <[u8; 32]>::from(transmission_key);
128        let v_bytes = value.to_bytes();
129
130        s.extend(g_d_bytes);
131        s.extend(pk_d_bytes);
132        s.extend(v_bytes);
133
134        let rcm = CommitmentRandomness(generate_trapdoor(csprng)?);
135
136        Ok((
137            rcm,
138            NoteCommitment::from(windowed_pedersen_commitment(rcm.0, &s)),
139        ))
140    }
141
142    /// Hash Extractor for Jubjub (?)
143    ///
144    /// <https://zips.z.cash/protocol/protocol.pdf#concreteextractorjubjub>
145    pub fn extract_u(&self) -> jubjub::Fq {
146        self.0.get_u()
147    }
148}
149
150/// A Homomorphic Pedersen commitment to the value of a note.
151///
152/// This can be used as an intermediate value in some computations. For the
153/// type actually stored in Spend and Output descriptions, see
154/// [`NotSmallOrderValueCommitment`].
155///
156/// <https://zips.z.cash/protocol/protocol.pdf#concretehomomorphiccommit>
157#[derive(Clone, Copy, Deserialize, PartialEq, Eq, Serialize)]
158#[cfg_attr(any(test, feature = "proptest-impl"), derive(Default))]
159pub struct ValueCommitment(#[serde(with = "serde_helpers::AffinePoint")] jubjub::AffinePoint);
160
161impl<'a> std::ops::Add<&'a ValueCommitment> for ValueCommitment {
162    type Output = Self;
163
164    fn add(self, rhs: &'a ValueCommitment) -> Self::Output {
165        self + *rhs
166    }
167}
168
169impl std::ops::Add<ValueCommitment> for ValueCommitment {
170    type Output = Self;
171
172    fn add(self, rhs: ValueCommitment) -> Self::Output {
173        let value = self.0.to_extended() + rhs.0.to_extended();
174        ValueCommitment(value.into())
175    }
176}
177
178impl std::ops::AddAssign<ValueCommitment> for ValueCommitment {
179    fn add_assign(&mut self, rhs: ValueCommitment) {
180        *self = *self + rhs
181    }
182}
183
184impl fmt::Debug for ValueCommitment {
185    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
186        f.debug_struct("ValueCommitment")
187            .field("u", &hex::encode(self.0.get_u().to_bytes()))
188            .field("v", &hex::encode(self.0.get_v().to_bytes()))
189            .finish()
190    }
191}
192
193impl From<jubjub::ExtendedPoint> for ValueCommitment {
194    /// Convert a Jubjub point into a ValueCommitment.
195    fn from(extended_point: jubjub::ExtendedPoint) -> Self {
196        Self(jubjub::AffinePoint::from(extended_point))
197    }
198}
199
200/// LEBS2OSP256(repr_J(cv))
201///
202/// <https://zips.z.cash/protocol/protocol.pdf#spendencoding>
203/// <https://zips.z.cash/protocol/protocol.pdf#jubjub>
204impl From<ValueCommitment> for [u8; 32] {
205    fn from(cm: ValueCommitment) -> [u8; 32] {
206        cm.0.to_bytes()
207    }
208}
209
210impl<'a> std::ops::Sub<&'a ValueCommitment> for ValueCommitment {
211    type Output = Self;
212
213    fn sub(self, rhs: &'a ValueCommitment) -> Self::Output {
214        self - *rhs
215    }
216}
217
218impl std::ops::Sub<ValueCommitment> for ValueCommitment {
219    type Output = Self;
220
221    fn sub(self, rhs: ValueCommitment) -> Self::Output {
222        ValueCommitment((self.0.to_extended() - rhs.0.to_extended()).into())
223    }
224}
225
226impl std::ops::SubAssign<ValueCommitment> for ValueCommitment {
227    fn sub_assign(&mut self, rhs: ValueCommitment) {
228        *self = *self - rhs;
229    }
230}
231
232impl std::iter::Sum for ValueCommitment {
233    fn sum<I>(iter: I) -> Self
234    where
235        I: Iterator<Item = Self>,
236    {
237        iter.fold(
238            ValueCommitment(jubjub::AffinePoint::identity()),
239            std::ops::Add::add,
240        )
241    }
242}
243
244/// LEBS2OSP256(repr_J(cv))
245///
246/// <https://zips.z.cash/protocol/protocol.pdf#spendencoding>
247/// <https://zips.z.cash/protocol/protocol.pdf#jubjub>
248impl TryFrom<[u8; 32]> for ValueCommitment {
249    type Error = &'static str;
250
251    fn try_from(bytes: [u8; 32]) -> Result<Self, Self::Error> {
252        let possible_point = jubjub::AffinePoint::from_bytes(bytes);
253
254        if possible_point.is_some().into() {
255            let point = possible_point.unwrap();
256            Ok(ExtendedPoint::from(point).into())
257        } else {
258            Err("Invalid jubjub::AffinePoint value")
259        }
260    }
261}
262
263impl ValueCommitment {
264    /// Generate a new _ValueCommitment_.
265    ///
266    /// <https://zips.z.cash/protocol/protocol.pdf#concretehomomorphiccommit>
267    pub fn randomized<T>(csprng: &mut T, value: Amount) -> Result<Self, RandError>
268    where
269        T: RngCore + CryptoRng,
270    {
271        let rcv = generate_trapdoor(csprng)?;
272
273        Ok(Self::new(rcv, value))
274    }
275
276    /// Generate a new _ValueCommitment_ from an existing _rcv_ on a _value_.
277    ///
278    /// <https://zips.z.cash/protocol/protocol.pdf#concretehomomorphiccommit>
279    #[allow(non_snake_case)]
280    pub fn new(rcv: jubjub::Fr, value: Amount) -> Self {
281        let v = jubjub::Fr::from(value);
282        Self::from(*V * v + *R * rcv)
283    }
284}
285
286lazy_static! {
287    static ref V: ExtendedPoint = find_group_hash(*b"Zcash_cv", b"v");
288    static ref R: ExtendedPoint = find_group_hash(*b"Zcash_cv", b"r");
289}
290
291/// A Homomorphic Pedersen commitment to the value of a note, used in Spend and
292/// Output descriptions.
293///
294/// Elements that are of small order are not allowed. This is a separate
295/// consensus rule and not intrinsic of value commitments; which is why this
296/// type exists.
297///
298/// This is denoted by `cv` in the specification.
299///
300/// <https://zips.z.cash/protocol/protocol.pdf#spenddesc>
301/// <https://zips.z.cash/protocol/protocol.pdf#outputdesc>
302#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Serialize)]
303#[cfg_attr(any(test, feature = "proptest-impl"), derive(Default))]
304pub struct NotSmallOrderValueCommitment(ValueCommitment);
305
306impl TryFrom<ValueCommitment> for NotSmallOrderValueCommitment {
307    type Error = &'static str;
308
309    /// Convert a ValueCommitment into a NotSmallOrderValueCommitment.
310    ///
311    /// Returns an error if the point is of small order.
312    ///
313    /// # Consensus
314    ///
315    /// > cv and rk [MUST NOT be of small order][1], i.e. \[h_J\]cv MUST NOT be 𝒪_J
316    /// > and \[h_J\]rk MUST NOT be 𝒪_J.
317    ///
318    /// > cv and epk [MUST NOT be of small order][2], i.e. \[h_J\]cv MUST NOT be 𝒪_J
319    /// > and \[ℎ_J\]epk MUST NOT be 𝒪_J.
320    ///
321    /// [1]: https://zips.z.cash/protocol/protocol.pdf#spenddesc
322    /// [2]: https://zips.z.cash/protocol/protocol.pdf#outputdesc
323    fn try_from(value_commitment: ValueCommitment) -> Result<Self, Self::Error> {
324        if value_commitment.0.is_small_order().into() {
325            Err("jubjub::AffinePoint value for Sapling ValueCommitment is of small order")
326        } else {
327            Ok(Self(value_commitment))
328        }
329    }
330}
331
332impl TryFrom<jubjub::ExtendedPoint> for NotSmallOrderValueCommitment {
333    type Error = &'static str;
334
335    /// Convert a Jubjub point into a NotSmallOrderValueCommitment.
336    fn try_from(extended_point: jubjub::ExtendedPoint) -> Result<Self, Self::Error> {
337        ValueCommitment::from(extended_point).try_into()
338    }
339}
340
341impl From<NotSmallOrderValueCommitment> for ValueCommitment {
342    fn from(cv: NotSmallOrderValueCommitment) -> Self {
343        cv.0
344    }
345}
346
347impl From<NotSmallOrderValueCommitment> for jubjub::AffinePoint {
348    fn from(cv: NotSmallOrderValueCommitment) -> Self {
349        cv.0 .0
350    }
351}
352
353impl ZcashSerialize for NotSmallOrderValueCommitment {
354    fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
355        writer.write_all(&<[u8; 32]>::from(self.0)[..])?;
356        Ok(())
357    }
358}
359
360impl ZcashDeserialize for NotSmallOrderValueCommitment {
361    fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
362        let vc = ValueCommitment::try_from(reader.read_32_bytes()?)
363            .map_err(SerializationError::Parse)?;
364        vc.try_into().map_err(SerializationError::Parse)
365    }
366}
367
368#[cfg(test)]
369mod tests {
370
371    use std::ops::Neg;
372
373    use super::*;
374
375    #[test]
376    fn pedersen_hash_to_point_test_vectors() {
377        let _init_guard = zebra_test::init();
378
379        const D: [u8; 8] = *b"Zcash_PH";
380
381        for test_vector in test_vectors::TEST_VECTORS.iter() {
382            let result = jubjub::AffinePoint::from(pedersen_hash_to_point(
383                D,
384                &test_vector.input_bits.clone(),
385            ));
386
387            assert_eq!(result, test_vector.output_point);
388        }
389    }
390
391    #[test]
392    fn add() {
393        let _init_guard = zebra_test::init();
394
395        let identity = ValueCommitment(jubjub::AffinePoint::identity());
396
397        let g = ValueCommitment(jubjub::AffinePoint::from_raw_unchecked(
398            jubjub::Fq::from_raw([
399                0xe4b3_d35d_f1a7_adfe,
400                0xcaf5_5d1b_29bf_81af,
401                0x8b0f_03dd_d60a_8187,
402                0x62ed_cbb8_bf37_87c8,
403            ]),
404            jubjub::Fq::from_raw([
405                0x0000_0000_0000_000b,
406                0x0000_0000_0000_0000,
407                0x0000_0000_0000_0000,
408                0x0000_0000_0000_0000,
409            ]),
410        ));
411
412        assert_eq!(identity + g, g);
413    }
414
415    #[test]
416    fn add_assign() {
417        let _init_guard = zebra_test::init();
418
419        let mut identity = ValueCommitment(jubjub::AffinePoint::identity());
420
421        let g = ValueCommitment(jubjub::AffinePoint::from_raw_unchecked(
422            jubjub::Fq::from_raw([
423                0xe4b3_d35d_f1a7_adfe,
424                0xcaf5_5d1b_29bf_81af,
425                0x8b0f_03dd_d60a_8187,
426                0x62ed_cbb8_bf37_87c8,
427            ]),
428            jubjub::Fq::from_raw([
429                0x0000_0000_0000_000b,
430                0x0000_0000_0000_0000,
431                0x0000_0000_0000_0000,
432                0x0000_0000_0000_0000,
433            ]),
434        ));
435
436        identity += g;
437        let new_g = identity;
438
439        assert_eq!(new_g, g);
440    }
441
442    #[test]
443    fn sub() {
444        let _init_guard = zebra_test::init();
445
446        let g_point = jubjub::AffinePoint::from_raw_unchecked(
447            jubjub::Fq::from_raw([
448                0xe4b3_d35d_f1a7_adfe,
449                0xcaf5_5d1b_29bf_81af,
450                0x8b0f_03dd_d60a_8187,
451                0x62ed_cbb8_bf37_87c8,
452            ]),
453            jubjub::Fq::from_raw([
454                0x0000_0000_0000_000b,
455                0x0000_0000_0000_0000,
456                0x0000_0000_0000_0000,
457                0x0000_0000_0000_0000,
458            ]),
459        );
460
461        let identity = ValueCommitment(jubjub::AffinePoint::identity());
462
463        let g = ValueCommitment(g_point);
464
465        assert_eq!(identity - g, ValueCommitment(g_point.neg()));
466    }
467
468    #[test]
469    fn sub_assign() {
470        let _init_guard = zebra_test::init();
471
472        let g_point = jubjub::AffinePoint::from_raw_unchecked(
473            jubjub::Fq::from_raw([
474                0xe4b3_d35d_f1a7_adfe,
475                0xcaf5_5d1b_29bf_81af,
476                0x8b0f_03dd_d60a_8187,
477                0x62ed_cbb8_bf37_87c8,
478            ]),
479            jubjub::Fq::from_raw([
480                0x0000_0000_0000_000b,
481                0x0000_0000_0000_0000,
482                0x0000_0000_0000_0000,
483                0x0000_0000_0000_0000,
484            ]),
485        );
486
487        let mut identity = ValueCommitment(jubjub::AffinePoint::identity());
488
489        let g = ValueCommitment(g_point);
490
491        identity -= g;
492        let new_g = identity;
493
494        assert_eq!(new_g, ValueCommitment(g_point.neg()));
495    }
496
497    #[test]
498    fn sum() {
499        let _init_guard = zebra_test::init();
500
501        let g_point = jubjub::AffinePoint::from_raw_unchecked(
502            jubjub::Fq::from_raw([
503                0xe4b3_d35d_f1a7_adfe,
504                0xcaf5_5d1b_29bf_81af,
505                0x8b0f_03dd_d60a_8187,
506                0x62ed_cbb8_bf37_87c8,
507            ]),
508            jubjub::Fq::from_raw([
509                0x0000_0000_0000_000b,
510                0x0000_0000_0000_0000,
511                0x0000_0000_0000_0000,
512                0x0000_0000_0000_0000,
513            ]),
514        );
515
516        let g = ValueCommitment(g_point);
517        let other_g = ValueCommitment(g_point);
518
519        let sum: ValueCommitment = vec![g, other_g].into_iter().sum();
520
521        let doubled_g = ValueCommitment(g_point.to_extended().double().into());
522
523        assert_eq!(sum, doubled_g);
524    }
525}