zebra_chain/orchard/
sinsemilla.rs

1//! Sinsemilla hash functions and helpers.
2
3use bitvec::prelude::*;
4use halo2::{
5    arithmetic::{Coordinates, CurveAffine, CurveExt},
6    pasta::{group::Group, pallas},
7};
8
9/// [Coordinate Extractor for Pallas][concreteextractorpallas]
10///
11/// ExtractP: P → P𝑥 such that ExtractP(𝑃) = 𝑥(𝑃) mod 𝑞P.
12///
13/// ExtractP returns the type P𝑥 which is precise for its range, unlike
14/// ExtractJ(𝑟) which returns a bit sequence.
15///
16/// [concreteextractorpallas]: https://zips.z.cash/protocol/nu5.pdf#concreteextractorpallas
17pub fn extract_p(point: pallas::Point) -> pallas::Base {
18    let option: Option<Coordinates<pallas::Affine>> =
19        pallas::Affine::from(point).coordinates().into();
20
21    match option {
22        // If Some, it's not the identity.
23        Some(coordinates) => *coordinates.x(),
24        _ => pallas::Base::zero(),
25    }
26}
27
28/// Extract⊥ P: P ∪ {⊥} → P𝑥 ∪ {⊥} such that
29///
30///  Extract⊥ P(︀⊥)︀ = ⊥
31///  Extract⊥ P(︀𝑃: P)︀ = ExtractP(𝑃).
32///
33/// <https://zips.z.cash/protocol/nu5.pdf#concreteextractorpallas>
34pub fn extract_p_bottom(maybe_point: Option<pallas::Point>) -> Option<pallas::Base> {
35    // Maps an Option<T> to Option<U> by applying a function to a contained value.
36    maybe_point.map(extract_p)
37}
38
39/// GroupHash into Pallas, aka _GroupHash^P_
40///
41/// Produces a random point in the Pallas curve.  The first input element acts
42/// as a domain separator to distinguish uses of the group hash for different
43/// purposes; the second input element is the message.
44///
45/// <https://zips.z.cash/protocol/nu5.pdf#concretegrouphashpallasandvesta>
46#[allow(non_snake_case)]
47pub fn pallas_group_hash(D: &[u8], M: &[u8]) -> pallas::Point {
48    let domain_separator = std::str::from_utf8(D).unwrap();
49
50    pallas::Point::hash_to_curve(domain_separator)(M)
51}
52
53/// Q(D) := GroupHash^P(︀“z.cash:SinsemillaQ”, D)
54///
55/// <https://zips.z.cash/protocol/nu5.pdf#concretesinsemillahash>
56#[allow(non_snake_case)]
57fn Q(D: &[u8]) -> pallas::Point {
58    pallas_group_hash(b"z.cash:SinsemillaQ", D)
59}
60
61/// S(j) := GroupHash^P(︀“z.cash:SinsemillaS”, LEBS2OSP32(I2LEBSP32(j)))
62///
63/// S: {0 .. 2^k - 1} -> P^*, aka 10 bits hashed into the group
64///
65/// <https://zips.z.cash/protocol/nu5.pdf#concretesinsemillahash>
66#[allow(non_snake_case)]
67fn S(j: &BitSlice<u8, Lsb0>) -> pallas::Point {
68    // The value of j is a 10-bit value, therefore must never exceed 2^10 in
69    // value.
70    assert_eq!(j.len(), 10);
71
72    // I2LEOSP_32(𝑗)
73    let mut leosp_32_j = [0u8; 4];
74    leosp_32_j[..2].copy_from_slice(j.to_bitvec().as_raw_slice());
75
76    pallas_group_hash(b"z.cash:SinsemillaS", &leosp_32_j)
77}
78
79/// Incomplete addition on the Pallas curve.
80///
81/// P ∪ {⊥} × P ∪ {⊥} → P ∪ {⊥}
82///
83/// <https://zips.z.cash/protocol/protocol.pdf#concretesinsemillahash>
84fn incomplete_addition(
85    left: Option<pallas::Point>,
86    right: Option<pallas::Point>,
87) -> Option<pallas::Point> {
88    let identity = pallas::Point::identity();
89
90    match (left, right) {
91        (None, _) | (_, None) => None,
92        (Some(l), _) if l == identity => None,
93        (_, Some(r)) if r == identity => None,
94        (Some(l), Some(r)) if l == r => None,
95        // The inverse of l, (x, -y)
96        (Some(l), Some(r)) if l == -r => None,
97        (Some(l), Some(r)) => Some(l + r),
98    }
99}
100
101/// "...an algebraic hash function with collision resistance (for fixed input
102/// length) derived from assumed hardness of the Discrete Logarithm Problem on
103/// the Pallas curve."
104///
105/// SinsemillaHash is used in the definitions of Sinsemilla commitments and of
106/// the Sinsemilla hash for the Orchard incremental Merkle tree (§ 5.4.1.3
107/// ‘MerkleCRH^Orchard Hash Function’).
108///
109/// SinsemillaHashToPoint(𝐷: B^Y^\[N\] , 𝑀 : B ^[{0 .. 𝑘·𝑐}] ) → P ∪ {⊥}
110///
111/// <https://zips.z.cash/protocol/nu5.pdf#concretesinsemillahash>
112///
113/// # Panics
114///
115/// If `M` is greater than `k*c = 2530` bits.
116#[allow(non_snake_case)]
117pub fn sinsemilla_hash_to_point(D: &[u8], M: &BitVec<u8, Lsb0>) -> Option<pallas::Point> {
118    let k = 10;
119    let c = 253;
120
121    assert!(M.len() <= k * c);
122
123    let mut acc = Some(Q(D));
124
125    // Split M into n segments of k bits, where k = 10 and c = 253, padding
126    // the last segment with zeros.
127    //
128    // https://zips.z.cash/protocol/nu5.pdf#concretesinsemillahash
129    for chunk in M.chunks(k) {
130        // Pad each chunk with zeros.
131        let mut store = [0u8; 2];
132        let bits = BitSlice::<_, Lsb0>::from_slice_mut(&mut store);
133        bits[..chunk.len()].copy_from_bitslice(chunk);
134
135        acc = incomplete_addition(incomplete_addition(acc, Some(S(&bits[..k]))), acc);
136    }
137
138    acc
139}
140
141/// Sinsemilla Hash Function
142///
143/// "SinsemillaHash is an algebraic hash function with collision resistance (for
144/// fixed input length) derived from assumed hardness of the Discrete Logarithm
145/// Problem. It is designed by Sean Bowe and Daira Hopwood. The motivation for
146/// introducing a new discrete-log-based hash function (rather than using
147/// PedersenHash) is to make efcient use of the lookups available in recent
148/// proof systems including Halo 2."
149///
150/// SinsemillaHash: B^Y^\[N\] × B[{0 .. 𝑘·𝑐}] → P_𝑥 ∪ {⊥}
151///
152/// <https://zips.z.cash/protocol/nu5.pdf#concretesinsemillahash>
153///
154/// # Panics
155///
156/// If `M` is greater than `k*c = 2530` bits in `sinsemilla_hash_to_point`.
157#[allow(non_snake_case)]
158pub fn sinsemilla_hash(D: &[u8], M: &BitVec<u8, Lsb0>) -> Option<pallas::Base> {
159    extract_p_bottom(sinsemilla_hash_to_point(D, M))
160}
161
162#[cfg(test)]
163mod tests {
164
165    use super::*;
166    use crate::orchard::tests::vectors;
167
168    #[cfg(test)]
169    fn x_from_str(s: &str) -> pallas::Base {
170        use halo2::pasta::group::ff::PrimeField;
171
172        pallas::Base::from_str_vartime(s).unwrap()
173    }
174
175    #[test]
176    #[allow(non_snake_case)]
177    fn sinsemilla_single_test_vector() {
178        use halo2::pasta::group::Curve;
179
180        let D = b"z.cash:test-Sinsemilla";
181        let M = bitvec![
182            u8, Lsb0; 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0,
183            1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0,
184        ];
185
186        let test_vector = pallas::Affine::from_xy(
187            x_from_str(
188                "19681977528872088480295086998934490146368213853811658798708435106473481753752",
189            ),
190            x_from_str(
191                "14670850419772526047574141291705097968771694788047376346841674072293161339903",
192            ),
193        )
194        .unwrap();
195
196        assert_eq!(
197            sinsemilla_hash_to_point(&D[..], &M).expect("").to_affine(),
198            test_vector
199        )
200    }
201
202    // Checks Sinsemilla hashes to point and to bytes (aka the x-coordinate
203    // bytes of a point) with:
204    // - One of two domains.
205    // - Random message lengths between 0 and 255 bytes.
206    // - Random message bits.
207    #[test]
208    #[allow(non_snake_case)]
209    fn sinsemilla_hackworks_test_vectors() {
210        use halo2::pasta::group::{ff::PrimeField, GroupEncoding};
211
212        for tv in tests::vectors::SINSEMILLA.iter() {
213            let D = tv.domain.as_slice();
214            let M: &BitVec<u8, Lsb0> = &tv.msg.iter().collect();
215
216            assert_eq!(
217                sinsemilla_hash_to_point(D, M).expect("should not fail per Theorem 5.4.4"),
218                pallas::Point::from_bytes(&tv.point).unwrap()
219            );
220
221            assert_eq!(
222                sinsemilla_hash(D, M).expect("should not fail per Theorem 5.4.4"),
223                pallas::Base::from_repr(tv.hash).unwrap()
224            )
225        }
226    }
227
228    // Checks Pallas group hashes with:
229    // - One of two domains.
230    // - Random message lengths between 0 and 255 bytes.
231    // - Random message contents.
232    #[test]
233    #[allow(non_snake_case)]
234    fn sinsemilla_hackworks_group_hash_test_vectors() {
235        use halo2::pasta::group::GroupEncoding;
236
237        for tv in tests::vectors::GROUP_HASHES.iter() {
238            let D = tv.domain.as_slice();
239            let M = tv.msg.as_slice();
240
241            assert_eq!(
242                pallas_group_hash(D, M),
243                pallas::Point::from_bytes(&tv.point).unwrap()
244            );
245        }
246    }
247}