zebra_chain/sapling/
commitment.rs

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