Skip to content

Commit

Permalink
Clean up hash-to-field (arkworks-rs#678)
Browse files Browse the repository at this point in the history
Co-authored-by: Pratyush Mishra <[email protected]>
Co-authored-by: ¨Jeff <¨[email protected]¨>
  • Loading branch information
3 people authored and aleasims committed Oct 18, 2023
1 parent d0f219c commit 9469e1b
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 91 deletions.
2 changes: 1 addition & 1 deletion ec/src/hashing/map_to_curve_hasher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ where
// 5. P = clear_cofactor(R)
// 6. return P

let rand_field_elems = self.field_hasher.hash_to_field(msg, 2);
let rand_field_elems = self.field_hasher.hash_to_field::<2>(msg);

let rand_curve_elem_0 = M2C::map_to_curve(rand_field_elems[0])?;
let rand_curve_elem_1 = M2C::map_to_curve(rand_field_elems[1])?;
Expand Down
1 change: 1 addition & 0 deletions ff/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ ark-ff-asm.workspace = true
ark-ff-macros.workspace = true
ark-std.workspace = true
ark-serialize.workspace = true
arrayvec = { version = "0.7", default-features = false }
derivative = { workspace = true, features = ["use_core"] }
num-traits.workspace = true
paste.workspace = true
Expand Down
133 changes: 77 additions & 56 deletions ff/src/fields/field_hashers/expander/mod.rs
Original file line number Diff line number Diff line change
@@ -1,99 +1,119 @@
// The below implementation is a rework of https://github.com/armfazh/h2c-rust-ref
// With some optimisations

use core::marker::PhantomData;

use ark_std::vec::Vec;
use digest::{DynDigest, ExtendableOutput, Update};

use arrayvec::ArrayVec;
use digest::{ExtendableOutput, FixedOutputReset, Update};

pub trait Expander {
fn construct_dst_prime(&self) -> Vec<u8>;
fn expand(&self, msg: &[u8], length: usize) -> Vec<u8>;
}
const MAX_DST_LENGTH: usize = 255;

const LONG_DST_PREFIX: [u8; 17] = [
//'H', '2', 'C', '-', 'O', 'V', 'E', 'R', 'S', 'I', 'Z', 'E', '-', 'D', 'S', 'T', '-',
0x48, 0x32, 0x43, 0x2d, 0x4f, 0x56, 0x45, 0x52, 0x53, 0x49, 0x5a, 0x45, 0x2d, 0x44, 0x53, 0x54,
0x2d,
];
const LONG_DST_PREFIX: &[u8; 17] = b"H2C-OVERSIZE-DST-";

pub(super) struct ExpanderXof<T: Update + Clone + ExtendableOutput> {
pub(super) xofer: T,
pub(super) dst: Vec<u8>,
pub(super) k: usize,
}
/// Implements section [5.3.3](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-5.3.3)
/// "Using DSTs longer than 255 bytes" of the
/// [IRTF CFRG hash-to-curve draft #16](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-5.3.3).
pub struct DST(arrayvec::ArrayVec<u8, MAX_DST_LENGTH>);

impl<T: Update + Clone + ExtendableOutput> Expander for ExpanderXof<T> {
fn construct_dst_prime(&self) -> Vec<u8> {
let mut dst_prime = if self.dst.len() > MAX_DST_LENGTH {
let mut xofer = self.xofer.clone();
xofer.update(&LONG_DST_PREFIX.clone());
xofer.update(&self.dst);
xofer.finalize_boxed((2 * self.k + 7) >> 3).to_vec()
impl DST {
pub fn new_xmd<H: FixedOutputReset + Default>(dst: &[u8]) -> DST {
let array = if dst.len() > MAX_DST_LENGTH {
let mut long = H::default();
long.update(&LONG_DST_PREFIX[..]);
long.update(&dst);
ArrayVec::try_from(long.finalize_fixed().as_ref()).unwrap()
} else {
self.dst.clone()
ArrayVec::try_from(dst).unwrap()
};
dst_prime.push(dst_prime.len() as u8);
dst_prime
DST(array)
}
fn expand(&self, msg: &[u8], n: usize) -> Vec<u8> {
let dst_prime = self.construct_dst_prime();
let lib_str = &[((n >> 8) & 0xFF) as u8, (n & 0xFF) as u8];

let mut xofer = self.xofer.clone();
pub fn new_xof<H: ExtendableOutput + Default>(dst: &[u8], k: usize) -> DST {
let array = if dst.len() > MAX_DST_LENGTH {
let mut long = H::default();
long.update(&LONG_DST_PREFIX[..]);
long.update(&dst);

let mut new_dst = [0u8; MAX_DST_LENGTH];
let new_dst = &mut new_dst[0..((2 * k + 7) >> 3)];
long.finalize_xof_into(new_dst);
ArrayVec::try_from(&*new_dst).unwrap()
} else {
ArrayVec::try_from(dst).unwrap()
};
DST(array)
}

pub fn update<H: Update>(&self, h: &mut H) {
h.update(self.0.as_ref());
// I2OSP(len,1) https://www.rfc-editor.org/rfc/rfc8017.txt
h.update(&[self.0.len() as u8]);
}
}

pub(super) struct ExpanderXof<H: ExtendableOutput + Clone + Default> {
pub(super) xofer: PhantomData<H>,
pub(super) dst: Vec<u8>,
pub(super) k: usize,
}

impl<H: ExtendableOutput + Clone + Default> Expander for ExpanderXof<H> {
fn expand(&self, msg: &[u8], n: usize) -> Vec<u8> {
let mut xofer = H::default();
xofer.update(msg);
xofer.update(lib_str);
xofer.update(&dst_prime);
xofer.finalize_boxed(n).to_vec()

// I2OSP(len,2) https://www.rfc-editor.org/rfc/rfc8017.txt
let lib_str = (n as u16).to_be_bytes();
xofer.update(&lib_str);

DST::new_xof::<H>(self.dst.as_ref(), self.k).update(&mut xofer);
xofer.finalize_boxed(n).into_vec()
}
}

pub(super) struct ExpanderXmd<T: DynDigest + Clone> {
pub(super) hasher: T,
pub(super) struct ExpanderXmd<H: FixedOutputReset + Default + Clone> {
pub(super) hasher: PhantomData<H>,
pub(super) dst: Vec<u8>,
pub(super) block_size: usize,
}

impl<T: DynDigest + Clone> Expander for ExpanderXmd<T> {
fn construct_dst_prime(&self) -> Vec<u8> {
let mut dst_prime = if self.dst.len() > MAX_DST_LENGTH {
let mut hasher = self.hasher.clone();
hasher.update(&LONG_DST_PREFIX);
hasher.update(&self.dst);
hasher.finalize_reset().to_vec()
} else {
self.dst.clone()
};
dst_prime.push(dst_prime.len() as u8);
dst_prime
}
static Z_PAD: [u8; 256] = [0u8; 256];

impl<H: FixedOutputReset + Default + Clone> Expander for ExpanderXmd<H> {
fn expand(&self, msg: &[u8], n: usize) -> Vec<u8> {
let mut hasher = self.hasher.clone();
use digest::typenum::Unsigned;
// output size of the hash function, e.g. 32 bytes = 256 bits for sha2::Sha256
let b_len = hasher.output_size();
let b_len = H::OutputSize::to_usize();
let ell = (n + (b_len - 1)) / b_len;
assert!(
ell <= 255,
"The ratio of desired output to the output size of hash function is too large!"
);

let dst_prime = self.construct_dst_prime();
let z_pad: Vec<u8> = vec![0; self.block_size];
let dst_prime = DST::new_xmd::<H>(self.dst.as_ref());
// Represent `len_in_bytes` as a 2-byte array.
// As per I2OSP method outlined in https://tools.ietf.org/pdf/rfc8017.pdf,
// The program should abort if integer that we're trying to convert is too large.
assert!(n < (1 << 16), "Length should be smaller than 2^16");
let lib_str: [u8; 2] = (n as u16).to_be_bytes();

hasher.update(&z_pad);
let mut hasher = H::default();
hasher.update(&Z_PAD[0..self.block_size]);
hasher.update(msg);
hasher.update(&lib_str);
hasher.update(&[0u8]);
hasher.update(&dst_prime);
let b0 = hasher.finalize_reset();
dst_prime.update(&mut hasher);
let b0 = hasher.finalize_fixed_reset();

hasher.update(&b0);
hasher.update(&[1u8]);
hasher.update(&dst_prime);
let mut bi = hasher.finalize_reset();
dst_prime.update(&mut hasher);
let mut bi = hasher.finalize_fixed_reset();

let mut uniform_bytes: Vec<u8> = Vec::with_capacity(n);
uniform_bytes.extend_from_slice(&bi);
Expand All @@ -103,11 +123,12 @@ impl<T: DynDigest + Clone> Expander for ExpanderXmd<T> {
hasher.update(&[*l ^ *r]);
}
hasher.update(&[i as u8]);
hasher.update(&dst_prime);
bi = hasher.finalize_reset();
dst_prime.update(&mut hasher);
bi = hasher.finalize_fixed_reset();
uniform_bytes.extend_from_slice(&bi);
}
uniform_bytes[0..n].to_vec()
uniform_bytes.truncate(n);
uniform_bytes
}
}

Expand Down
11 changes: 6 additions & 5 deletions ff/src/fields/field_hashers/expander/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use sha3::{Shake128, Shake256};
use std::{
fs::{read_dir, File},
io::BufReader,
marker::PhantomData,
};

use super::{Expander, ExpanderXmd, ExpanderXof};
Expand Down Expand Up @@ -99,29 +100,29 @@ fn get_expander(id: ExpID, _dst: &[u8], k: usize) -> Box<dyn Expander> {
match id {
ExpID::XMD(h) => match h {
HashID::SHA256 => Box::new(ExpanderXmd {
hasher: Sha256::default(),
hasher: PhantomData::<Sha256>,
block_size: 64,
dst,
}),
HashID::SHA384 => Box::new(ExpanderXmd {
hasher: Sha384::default(),
hasher: PhantomData::<Sha384>,
block_size: 128,
dst,
}),
HashID::SHA512 => Box::new(ExpanderXmd {
hasher: Sha512::default(),
hasher: PhantomData::<Sha512>,
block_size: 128,
dst,
}),
},
ExpID::XOF(x) => match x {
XofID::SHAKE128 => Box::new(ExpanderXof {
xofer: Shake128::default(),
xofer: PhantomData::<Shake128>,
k,
dst,
}),
XofID::SHAKE256 => Box::new(ExpanderXof {
xofer: Shake256::default(),
xofer: PhantomData::<Shake256>,
k,
dst,
}),
Expand Down
64 changes: 38 additions & 26 deletions ff/src/fields/field_hashers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
mod expander;

use core::marker::PhantomData;

use crate::{Field, PrimeField};

use ark_std::vec::Vec;
use digest::DynDigest;
use digest::{FixedOutputReset, XofReader};
use expander::Expander;

use self::expander::ExpanderXmd;
Expand All @@ -17,8 +18,8 @@ pub trait HashToField<F: Field>: Sized {
/// * `domain` - bytes that get concatenated with the `msg` during hashing, in order to separate potentially interfering instantiations of the hasher.
fn new(domain: &[u8]) -> Self;

/// Hash an arbitrary `msg` to #`count` elements from field `F`.
fn hash_to_field(&self, msg: &[u8], count: usize) -> Vec<F>;
/// Hash an arbitrary `msg` to `N` elements of the field `F`.
fn hash_to_field<const N: usize>(&self, msg: &[u8]) -> [F; N];
}

/// This field hasher constructs a Hash-To-Field based on a fixed-output hash function,
Expand All @@ -33,16 +34,16 @@ pub trait HashToField<F: Field>: Sized {
/// use sha2::Sha256;
///
/// let hasher = <DefaultFieldHasher<Sha256> as HashToField<Fq>>::new(&[1, 2, 3]);
/// let field_elements: Vec<Fq> = hasher.hash_to_field(b"Hello, World!", 2);
/// let field_elements: [Fq; 2] = hasher.hash_to_field(b"Hello, World!");
///
/// assert_eq!(field_elements.len(), 2);
/// ```
pub struct DefaultFieldHasher<H: Default + DynDigest + Clone, const SEC_PARAM: usize = 128> {
pub struct DefaultFieldHasher<H: FixedOutputReset + Default + Clone, const SEC_PARAM: usize = 128> {
expander: ExpanderXmd<H>,
len_per_base_elem: usize,
}

impl<F: Field, H: Default + DynDigest + Clone, const SEC_PARAM: usize> HashToField<F>
impl<F: Field, H: FixedOutputReset + Default + Clone, const SEC_PARAM: usize> HashToField<F>
for DefaultFieldHasher<H, SEC_PARAM>
{
fn new(dst: &[u8]) -> Self {
Expand All @@ -51,7 +52,7 @@ impl<F: Field, H: Default + DynDigest + Clone, const SEC_PARAM: usize> HashToFie
let len_per_base_elem = get_len_per_elem::<F, SEC_PARAM>();

let expander = ExpanderXmd {
hasher: H::default(),
hasher: PhantomData,
dst: dst.to_vec(),
block_size: len_per_base_elem,
};
Expand All @@ -62,38 +63,49 @@ impl<F: Field, H: Default + DynDigest + Clone, const SEC_PARAM: usize> HashToFie
}
}

fn hash_to_field(&self, message: &[u8], count: usize) -> Vec<F> {
fn hash_to_field<const N: usize>(&self, message: &[u8]) -> [F; N] {
let m = F::extension_degree() as usize;

// The user imposes a `count` of elements of F_p^m to output per input msg,
// The user requests `N` of elements of F_p^m to output per input msg,
// each field element comprising `m` BasePrimeField elements.
let len_in_bytes = count * m * self.len_per_base_elem;
let len_in_bytes = N * m * self.len_per_base_elem;
let uniform_bytes = self.expander.expand(message, len_in_bytes);

let mut output = Vec::with_capacity(count);
let mut base_prime_field_elems = Vec::with_capacity(m);
for i in 0..count {
base_prime_field_elems.clear();
for j in 0..m {
let cb = |i| {
let base_prime_field_elem = |j| {
let elm_offset = self.len_per_base_elem * (j + i * m);
let val = F::BasePrimeField::from_be_bytes_mod_order(
F::BasePrimeField::from_be_bytes_mod_order(
&uniform_bytes[elm_offset..][..self.len_per_base_elem],
);
base_prime_field_elems.push(val);
}
let f = F::from_base_prime_field_elems(base_prime_field_elems.drain(..)).unwrap();
output.push(f);
}

output
)
};
F::from_base_prime_field_elems((0..m).map(base_prime_field_elem)).unwrap()
};
ark_std::array::from_fn::<F, N, _>(cb)
}
}

pub fn hash_to_field<F: Field, H: XofReader, const SEC_PARAM: usize>(h: &mut H) -> F {
// The final output of `hash_to_field` will be an array of field
// elements from F::BaseField, each of size `len_per_elem`.
let len_per_base_elem = get_len_per_elem::<F, SEC_PARAM>();
// Rust *still* lacks alloca, hence this ugly hack.
let mut alloca = [0u8; 2048];
let alloca = &mut alloca[0..len_per_base_elem];

let m = F::extension_degree() as usize;

let base_prime_field_elem = |_| {
h.read(alloca);
F::BasePrimeField::from_be_bytes_mod_order(alloca)
};
F::from_base_prime_field_elems((0..m).map(base_prime_field_elem)).unwrap()
}

/// This function computes the length in bytes that a hash function should output
/// for hashing an element of type `Field`.
/// See section 5.1 and 5.3 of the
/// [IETF hash standardization draft](https://datatracker.ietf.org/doc/draft-irtf-cfrg-hash-to-curve/14/)
fn get_len_per_elem<F: Field, const SEC_PARAM: usize>() -> usize {
const fn get_len_per_elem<F: Field, const SEC_PARAM: usize>() -> usize {
// ceil(log(p))
let base_field_size_in_bits = F::BasePrimeField::MODULUS_BIT_SIZE as usize;
// ceil(log(p)) + security_parameter
Expand Down
6 changes: 3 additions & 3 deletions test-templates/src/h2c/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ macro_rules! test_h2c {

for v in data.vectors.iter() {
// first, hash-to-field tests
let got: Vec<$base_prime_field> =
hasher.hash_to_field(&v.msg.as_bytes(), 2 * $m);
let got: [$base_prime_field; { 2 * $m }] =
hasher.hash_to_field(&v.msg.as_bytes());
let want: Vec<$base_prime_field> =
v.u.iter().map(read_fq_vec).flatten().collect();
assert_eq!(got, want);
assert_eq!(got[..], *want);

// then, test curve points
let x = read_fq_vec(&v.p.x);
Expand Down

0 comments on commit 9469e1b

Please sign in to comment.