Separate methods for hashing with or without padding (#458)

* Separate methods for hashing with or without padding

This should be a tad better for for performance, and lets us do padding in a generic way, rather than each hash reimplementing it.

This also disables padding for public inputs. It seems unnecessary since the number of public inputs is fixed for any given instance.

* PR feedback

* update
This commit is contained in:
Daniel Lubarov 2022-02-04 13:08:57 -08:00 committed by GitHub
parent 659f1337f2
commit b6a60e721d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 43 additions and 57 deletions

View File

@ -115,14 +115,13 @@ fn fri_proof_of_work<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, c
(0..=F::NEG_ONE.to_canonical_u64())
.into_par_iter()
.find_any(|&i| {
C::InnerHasher::hash(
C::InnerHasher::hash_no_pad(
&current_hash
.elements
.iter()
.copied()
.chain(Some(F::from_canonical_u64(i)))
.collect_vec(),
false,
)
.elements[0]
.to_canonical_u64()

View File

@ -114,7 +114,7 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
let mut inputs = challenger.get_hash(self).elements.to_vec();
inputs.push(proof.pow_witness);
let hash = self.hash_n_to_m::<H>(inputs, 1, false)[0];
let hash = self.hash_n_to_m_no_pad::<H>(inputs, 1)[0];
self.assert_leading_zeros(
hash,
config.proof_of_work_bits + (64 - F::order().bits()) as u32,

View File

@ -18,7 +18,7 @@ pub fn hash_or_noop<F: RichField, P: PlonkyPermutation<F>>(inputs: Vec<F>) -> Ha
if inputs.len() <= 4 {
HashOut::from_partial(inputs)
} else {
hash_n_to_hash::<F, P>(&inputs, false)
hash_n_to_hash_no_pad::<F, P>(&inputs)
}
}
@ -28,34 +28,23 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
if inputs.len() <= 4 {
HashOutTarget::from_partial(inputs, zero)
} else {
self.hash_n_to_hash::<H>(inputs, false)
self.hash_n_to_hash_no_pad::<H>(inputs)
}
}
pub fn hash_n_to_hash<H: AlgebraicHasher<F>>(
pub fn hash_n_to_hash_no_pad<H: AlgebraicHasher<F>>(
&mut self,
inputs: Vec<Target>,
pad: bool,
) -> HashOutTarget {
HashOutTarget::from_vec(self.hash_n_to_m::<H>(inputs, 4, pad))
HashOutTarget::from_vec(self.hash_n_to_m_no_pad::<H>(inputs, 4))
}
pub fn hash_n_to_m<H: AlgebraicHasher<F>>(
pub fn hash_n_to_m_no_pad<H: AlgebraicHasher<F>>(
&mut self,
mut inputs: Vec<Target>,
inputs: Vec<Target>,
num_outputs: usize,
pad: bool,
) -> Vec<Target> {
let zero = self.zero();
let one = self.one();
if pad {
inputs.push(zero);
while (inputs.len() + 1) % SPONGE_WIDTH != 0 {
inputs.push(one);
}
inputs.push(zero);
}
let mut state = [zero; SPONGE_WIDTH];
@ -97,24 +86,12 @@ pub trait PlonkyPermutation<F: RichField> {
fn permute(input: [F; SPONGE_WIDTH]) -> [F; SPONGE_WIDTH];
}
/// If `pad` is enabled, the message is padded using the pad10*1 rule. In general this is required
/// for the hash to be secure, but it can safely be disabled in certain cases, like if the input
/// length is fixed.
pub fn hash_n_to_m<F: RichField, P: PlonkyPermutation<F>>(
/// Hash a message without any padding step. Note that this can enable length-extension attacks.
/// However, it is still collision-resistant in cases where the input has a fixed length.
pub fn hash_n_to_m_no_pad<F: RichField, P: PlonkyPermutation<F>>(
inputs: &[F],
num_outputs: usize,
pad: bool,
) -> Vec<F> {
if pad {
let mut padded_inputs = inputs.to_vec();
padded_inputs.push(F::ZERO);
while (padded_inputs.len() + 1) % SPONGE_WIDTH != 0 {
padded_inputs.push(F::ONE);
}
padded_inputs.push(F::ZERO);
return hash_n_to_m::<F, P>(&padded_inputs, num_outputs, false);
}
let mut state = [F::ZERO; SPONGE_WIDTH];
// Absorb all input chunks.
@ -136,9 +113,6 @@ pub fn hash_n_to_m<F: RichField, P: PlonkyPermutation<F>>(
}
}
pub fn hash_n_to_hash<F: RichField, P: PlonkyPermutation<F>>(
inputs: &[F],
pad: bool,
) -> HashOut<F> {
HashOut::from_vec(hash_n_to_m::<F, P>(inputs, 4, pad))
pub fn hash_n_to_hash_no_pad<F: RichField, P: PlonkyPermutation<F>>(inputs: &[F]) -> HashOut<F> {
HashOut::from_vec(hash_n_to_m_no_pad::<F, P>(inputs, 4))
}

View File

@ -56,7 +56,7 @@ impl<F: RichField, const N: usize> Hasher<F> for KeccakHash<N> {
type Hash = BytesHash<N>;
type Permutation = KeccakPermutation;
fn hash(input: &[F], _pad: bool) -> Self::Hash {
fn hash_no_pad(input: &[F]) -> Self::Hash {
let mut buffer = Buffer::new(Vec::new());
buffer.write_field_vec(input).unwrap();
let mut arr = [0; N];

View File

@ -32,7 +32,7 @@ pub(crate) fn verify_merkle_proof<F: RichField, H: Hasher<F>>(
proof: &MerkleProof<F, H>,
) -> Result<()> {
let mut index = leaf_index;
let mut current_digest = H::hash(&leaf_data, false);
let mut current_digest = H::hash_no_pad(&leaf_data);
for &sibling_digest in proof.siblings.iter() {
let bit = index & 1;
index >>= 1;

View File

@ -63,7 +63,7 @@ fn fill_subtree<F: RichField, H: Hasher<F>>(
) -> H::Hash {
assert_eq!(leaves.len(), digests_buf.len() / 2 + 1);
if digests_buf.is_empty() {
H::hash(&leaves[0], false)
H::hash_no_pad(&leaves[0])
} else {
// Layout is: left recursive output || left child digest
// || right child digest || right recursive output.
@ -99,7 +99,7 @@ fn fill_digests_buf<F: RichField, H: Hasher<F>>(
.par_iter_mut()
.zip(leaves)
.for_each(|(cap_buf, leaf)| {
cap_buf.write(H::hash(leaf, false));
cap_buf.write(H::hash_no_pad(leaf));
});
return;
}

View File

@ -66,7 +66,7 @@ pub(crate) fn decompress_merkle_proofs<F: RichField, H: Hasher<F>>(
for (&i, v) in leaves_indices.iter().zip(leaves_data) {
// Observe the leaves.
seen.insert(i + num_leaves, H::hash(v, false));
seen.insert(i + num_leaves, H::hash_no_pad(v));
}
// Iterators over the siblings.

View File

@ -9,7 +9,7 @@ use crate::gates::gate::Gate;
use crate::gates::poseidon::PoseidonGate;
use crate::gates::poseidon_mds::PoseidonMdsGate;
use crate::hash::hash_types::{HashOut, RichField};
use crate::hash::hashing::{compress, hash_n_to_hash, PlonkyPermutation, SPONGE_WIDTH};
use crate::hash::hashing::{compress, hash_n_to_hash_no_pad, PlonkyPermutation, SPONGE_WIDTH};
use crate::iop::ext_target::ExtensionTarget;
use crate::iop::target::{BoolTarget, Target};
use crate::plonk::circuit_builder::CircuitBuilder;
@ -633,8 +633,8 @@ impl<F: RichField> Hasher<F> for PoseidonHash {
type Hash = HashOut<F>;
type Permutation = PoseidonPermutation;
fn hash(input: &[F], pad: bool) -> Self::Hash {
hash_n_to_hash::<F, Self::Permutation>(input, pad)
fn hash_no_pad(input: &[F]) -> Self::Hash {
hash_n_to_hash_no_pad::<F, Self::Permutation>(input)
}
fn two_to_one(left: Self::Hash, right: Self::Hash) -> Self::Hash {

View File

@ -165,7 +165,7 @@ impl<F: RichField, H: Hasher<F>> Challenger<F, H> {
self.observe_extension_elements(&final_poly.coeffs);
let fri_pow_response = C::InnerHasher::hash(
let fri_pow_response = C::InnerHasher::hash_no_pad(
&self
.get_hash()
.elements
@ -173,7 +173,6 @@ impl<F: RichField, H: Hasher<F>> Challenger<F, H> {
.copied()
.chain(Some(pow_witness))
.collect::<Vec<_>>(),
false,
)
.elements[0];

View File

@ -621,7 +621,7 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
// those hash wires match the claimed public inputs.
let num_public_inputs = self.public_inputs.len();
let public_inputs_hash =
self.hash_n_to_hash::<C::InnerHasher>(self.public_inputs.clone(), true);
self.hash_n_to_hash_no_pad::<C::InnerHasher>(self.public_inputs.clone());
let pi_gate = self.add_gate(PublicInputGate, vec![]);
for (&hash_part, wire) in public_inputs_hash
.elements
@ -749,7 +749,7 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
constants_sigmas_cap.flatten(),
vec![/* Add other circuit data here */],
];
let circuit_digest = C::Hasher::hash(&circuit_digest_parts.concat(), false);
let circuit_digest = C::Hasher::hash_no_pad(&circuit_digest_parts.concat());
let common = CommonCircuitData {
config: self.config,

View File

@ -31,7 +31,21 @@ pub trait Hasher<F: RichField>: Sized + Clone + Debug + Eq + PartialEq {
/// Permutation used in the sponge construction.
type Permutation: PlonkyPermutation<F>;
fn hash(input: &[F], pad: bool) -> Self::Hash;
/// Hash a message without any padding step. Note that this can enable length-extension attacks.
/// However, it is still collision-resistant in cases where the input has a fixed length.
fn hash_no_pad(input: &[F]) -> Self::Hash;
/// Pad the message using the `pad10*1` rule, then hash it.
fn hash_pad(input: &[F]) -> Self::Hash {
let mut padded_input = input.to_vec();
padded_input.push(F::ONE);
while (padded_input.len() + 1) % SPONGE_WIDTH != 0 {
padded_input.push(F::ZERO);
}
padded_input.push(F::ONE);
Self::hash_no_pad(&padded_input)
}
fn two_to_one(left: Self::Hash, right: Self::Hash) -> Self::Hash;
}

View File

@ -91,7 +91,7 @@ impl<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, const D: usize>
pub(crate) fn get_public_inputs_hash(
&self,
) -> <<C as GenericConfig<D>>::InnerHasher as Hasher<F>>::Hash {
C::InnerHasher::hash(&self.public_inputs, true)
C::InnerHasher::hash_no_pad(&self.public_inputs)
}
pub fn to_bytes(&self) -> anyhow::Result<Vec<u8>> {
@ -207,7 +207,7 @@ impl<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, const D: usize>
pub(crate) fn get_public_inputs_hash(
&self,
) -> <<C as GenericConfig<D>>::InnerHasher as Hasher<F>>::Hash {
C::InnerHasher::hash(&self.public_inputs, true)
C::InnerHasher::hash_no_pad(&self.public_inputs)
}
pub fn to_bytes(&self) -> anyhow::Result<Vec<u8>> {

View File

@ -44,7 +44,7 @@ pub(crate) fn prove<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, co
);
let public_inputs = partition_witness.get_targets(&prover_data.public_inputs);
let public_inputs_hash = C::InnerHasher::hash(&public_inputs, true);
let public_inputs_hash = C::InnerHasher::hash_no_pad(&public_inputs);
if cfg!(debug_assertions) {
// Display the marked targets for debugging purposes.

View File

@ -27,7 +27,7 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
} = proof_with_pis;
assert_eq!(public_inputs.len(), inner_common_data.num_public_inputs);
let public_inputs_hash = self.hash_n_to_hash::<C::InnerHasher>(public_inputs, true);
let public_inputs_hash = self.hash_n_to_hash_no_pad::<C::InnerHasher>(public_inputs);
self.verify_proof(
proof,