//! Sinsemilla hash functions and helpers. use bitvec::prelude::*; use halo2::{ arithmetic::{Coordinates, CurveAffine, CurveExt}, pasta::{group::Group, pallas}, }; /// [Coordinate Extractor for Pallas][concreteextractorpallas] /// /// ExtractP: P → P𝑥 such that ExtractP(𝑃) = 𝑥(𝑃) mod 𝑞P. /// /// ExtractP returns the type P𝑥 which is precise for its range, unlike /// ExtractJ(𝑟) which returns a bit sequence. /// /// [concreteextractorpallas]: https://zips.z.cash/protocol/nu5.pdf#concreteextractorpallas pub fn extract_p(point: pallas::Point) -> pallas::Base { let option: Option> = pallas::Affine::from(point).coordinates().into(); match option { // If Some, it's not the identity. Some(coordinates) => *coordinates.x(), _ => pallas::Base::zero(), } } /// Extract⊥ P: P ∪ {⊥} → P𝑥 ∪ {⊥} such that /// /// Extract⊥ P(︀⊥)︀ = ⊥ /// Extract⊥ P(︀𝑃: P)︀ = ExtractP(𝑃). /// /// pub fn extract_p_bottom(maybe_point: Option) -> Option { // Maps an Option to Option by applying a function to a contained value. maybe_point.map(extract_p) } /// GroupHash into Pallas, aka _GroupHash^P_ /// /// Produces a random point in the Pallas curve. The first input element acts /// as a domain separator to distinguish uses of the group hash for different /// purposes; the second input element is the message. /// /// #[allow(non_snake_case)] pub fn pallas_group_hash(D: &[u8], M: &[u8]) -> pallas::Point { let domain_separator = std::str::from_utf8(D).unwrap(); pallas::Point::hash_to_curve(domain_separator)(M) } /// Q(D) := GroupHash^P(︀“z.cash:SinsemillaQ”, D) /// /// #[allow(non_snake_case)] fn Q(D: &[u8]) -> pallas::Point { pallas_group_hash(b"z.cash:SinsemillaQ", D) } /// S(j) := GroupHash^P(︀“z.cash:SinsemillaS”, LEBS2OSP32(I2LEBSP32(j))) /// /// S: {0 .. 2^k - 1} -> P^*, aka 10 bits hashed into the group /// /// #[allow(non_snake_case)] fn S(j: &BitSlice) -> pallas::Point { // The value of j is a 10-bit value, therefore must never exceed 2^10 in // value. assert_eq!(j.len(), 10); // I2LEOSP_32(𝑗) let mut leosp_32_j = [0u8; 4]; leosp_32_j[..2].copy_from_slice(j.to_bitvec().as_raw_slice()); pallas_group_hash(b"z.cash:SinsemillaS", &leosp_32_j) } /// Incomplete addition on the Pallas curve. /// /// P ∪ {⊥} × P ∪ {⊥} → P ∪ {⊥} /// /// fn incomplete_addition( left: Option, right: Option, ) -> Option { let identity = pallas::Point::identity(); match (left, right) { (None, _) | (_, None) => None, (Some(l), _) if l == identity => None, (_, Some(r)) if r == identity => None, (Some(l), Some(r)) if l == r => None, // The inverse of l, (x, -y) (Some(l), Some(r)) if l == -r => None, (Some(l), Some(r)) => Some(l + r), } } /// "...an algebraic hash function with collision resistance (for fixed input /// length) derived from assumed hardness of the Discrete Logarithm Problem on /// the Pallas curve." /// /// SinsemillaHash is used in the definitions of Sinsemilla commitments and of /// the Sinsemilla hash for the Orchard incremental Merkle tree (§ 5.4.1.3 /// ‘MerkleCRH^Orchard Hash Function’). /// /// SinsemillaHashToPoint(𝐷: B^Y^\[N\] , 𝑀 : B ^[{0 .. 𝑘·𝑐}] ) → P ∪ {⊥} /// /// /// /// # Panics /// /// If `M` is greater than `k*c = 2530` bits. #[allow(non_snake_case)] pub fn sinsemilla_hash_to_point(D: &[u8], M: &BitVec) -> Option { let k = 10; let c = 253; assert!(M.len() <= k * c); let mut acc = Some(Q(D)); // Split M into n segments of k bits, where k = 10 and c = 253, padding // the last segment with zeros. // // https://zips.z.cash/protocol/nu5.pdf#concretesinsemillahash for chunk in M.chunks(k) { // Pad each chunk with zeros. let mut store = [0u8; 2]; let bits = BitSlice::<_, Lsb0>::from_slice_mut(&mut store); bits[..chunk.len()].copy_from_bitslice(chunk); acc = incomplete_addition(incomplete_addition(acc, Some(S(&bits[..k]))), acc); } acc } /// Sinsemilla Hash Function /// /// "SinsemillaHash is an algebraic hash function with collision resistance (for /// fixed input length) derived from assumed hardness of the Discrete Logarithm /// Problem. It is designed by Sean Bowe and Daira Hopwood. The motivation for /// introducing a new discrete-log-based hash function (rather than using /// PedersenHash) is to make efcient use of the lookups available in recent /// proof systems including Halo 2." /// /// SinsemillaHash: B^Y^\[N\] × B[{0 .. 𝑘·𝑐}] → P_𝑥 ∪ {⊥} /// /// /// /// # Panics /// /// If `M` is greater than `k*c = 2530` bits in `sinsemilla_hash_to_point`. #[allow(non_snake_case)] pub fn sinsemilla_hash(D: &[u8], M: &BitVec) -> Option { extract_p_bottom(sinsemilla_hash_to_point(D, M)) } #[cfg(test)] mod tests { use super::*; use crate::orchard::tests::vectors; #[cfg(test)] fn x_from_str(s: &str) -> pallas::Base { use halo2::pasta::group::ff::PrimeField; pallas::Base::from_str_vartime(s).unwrap() } #[test] #[allow(non_snake_case)] fn single_test_vector() { use halo2::pasta::group::Curve; let D = b"z.cash:test-Sinsemilla"; let M = bitvec![ 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, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, ]; let test_vector = pallas::Affine::from_xy( x_from_str( "19681977528872088480295086998934490146368213853811658798708435106473481753752", ), x_from_str( "14670850419772526047574141291705097968771694788047376346841674072293161339903", ), ) .unwrap(); assert_eq!( sinsemilla_hash_to_point(&D[..], &M).expect("").to_affine(), test_vector ) } // Checks Sinsemilla hashes to point and to bytes (aka the x-coordinate // bytes of a point) with: // - One of two domains. // - Random message lengths between 0 and 255 bytes. // - Random message bits. #[test] #[allow(non_snake_case)] fn hackworks_test_vectors() { use halo2::pasta::group::{ff::PrimeField, GroupEncoding}; for tv in tests::vectors::SINSEMILLA.iter() { let D = tv.domain.as_slice(); let M: &BitVec = &tv.msg.iter().collect(); assert_eq!( sinsemilla_hash_to_point(D, M).expect("should not fail per Theorem 5.4.4"), pallas::Point::from_bytes(&tv.point).unwrap() ); assert_eq!( sinsemilla_hash(D, M).expect("should not fail per Theorem 5.4.4"), pallas::Base::from_repr(tv.hash).unwrap() ) } } // Checks Pallas group hashes with: // - One of two domains. // - Random message lengths between 0 and 255 bytes. // - Random message contents. #[test] #[allow(non_snake_case)] fn hackworks_group_hash_test_vectors() { use halo2::pasta::group::GroupEncoding; for tv in tests::vectors::GROUP_HASHES.iter() { let D = tv.domain.as_slice(); let M = tv.msg.as_slice(); assert_eq!( pallas_group_hash(D, M), pallas::Point::from_bytes(&tv.point).unwrap() ); } } }