Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean up hash-to-field #678

Merged
merged 26 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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] {
Pratyush marked this conversation as resolved.
Show resolved Hide resolved
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 {
Pratyush marked this conversation as resolved.
Show resolved Hide resolved
// 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
Loading