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}