Browse Source

feat: update RPO's padding rule to use that in the xHash paper (#318)

al-update-winterfell
Al-Kindi-0 6 months ago
committed by GitHub
parent
commit
a734dace1e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
8 changed files with 384 additions and 136 deletions
  1. +2
    -2
      CHANGELOG.md
  2. +8
    -7
      src/dsa/rpo_falcon512/keys/secret_key.rs
  3. +1
    -1
      src/hash/mod.rs
  4. +1
    -3
      src/hash/rescue/mod.rs
  5. +48
    -42
      src/hash/rescue/rpo/mod.rs
  6. +112
    -70
      src/hash/rescue/rpo/tests.rs
  7. +26
    -11
      src/hash/rescue/rpx/mod.rs
  8. +186
    -0
      src/hash/rescue/rpx/tests.rs

+ 2
- 2
CHANGELOG.md

@ -4,8 +4,8 @@
- Added `Mmr::open()` and `Mmr::peaks()` which rely on `Mmr::open_at()` and `Mmr::peaks()` respectively (#234).
- Standardised CI and Makefile across Miden repos (#323).
- Added `Smt::compute_mutations()` and `Smt::apply_mutations()` for validation-checked insertions (#327).
- [BREAKING] Changed return value of the `Mmr::verify()` and `MerklePath::verify()` from `bool` to
`Result<>` (#).
- Changed padding rule for RPO/RPX hash functions (#318).
- [BREAKING] Changed return value of the `Mmr::verify()` and `MerklePath::verify()` from `bool` to `Result<>` (#335).
- Added `is_empty()` functions to the `SimpleSmt` and `Smt` structures. Added `EMPTY_ROOT` constant to the `SparseMerkleTree` trait (#337).
## 0.10.1 (2024-09-13)

+ 8
- 7
src/dsa/rpo_falcon512/keys/secret_key.rs

@ -28,14 +28,15 @@ const WIDTH_SMALL_POLY_COEFFICIENT: usize = 6;
// SECRET KEY
// ================================================================================================
/// The secret key is a quadruple [[g, -f], [G, -F]] of polynomials with integer coefficients.
/// Represents the secret key for Falcon DSA.
///
/// Each polynomial is of degree at most N = 512 and computations with these polynomials are done
/// modulo the monic irreducible polynomial ϕ = x^N + 1. The secret key is a basis for a lattice
/// and has the property of being short with respect to a certain norm and an upper bound
/// appropriate for a given security parameter. The public key on the other hand is another basis
/// for the same lattice and can be described by a single polynomial h with integer coefficients
/// modulo ϕ. The two keys are related by the following relation:
/// The secret key is a quadruple [[g, -f], [G, -F]] of polynomials with integer coefficients. Each
/// polynomial is of degree at most N = 512 and computations with these polynomials is done modulo
/// the monic irreducible polynomial ϕ = x^N + 1. The secret key is a basis for a lattice and has
/// the property of being short with respect to a certain norm and an upper bound appropriate for
/// a given security parameter. The public key on the other hand is another basis for the same
/// lattice and can be described by a single polynomial h with integer coefficients modulo ϕ.
/// The two keys are related by the following relation:
///
/// 1. h = g /f [mod ϕ][mod p]
/// 2. f.G - g.F = p [mod ϕ]

+ 1
- 1
src/hash/mod.rs

@ -1,6 +1,6 @@
//! Cryptographic hash functions used by the Miden VM and the Miden rollup.
use super::{CubeExtension, Felt, FieldElement, StarkField, ONE, ZERO};
use super::{CubeExtension, Felt, FieldElement, StarkField, ZERO};
pub mod blake;

+ 1
- 3
src/hash/rescue/mod.rs

@ -1,8 +1,6 @@
use core::ops::Range;
use super::{
CubeExtension, Digest, ElementHasher, Felt, FieldElement, Hasher, StarkField, ONE, ZERO,
};
use super::{CubeExtension, Digest, ElementHasher, Felt, FieldElement, Hasher, StarkField, ZERO};
mod arch;
pub use arch::optimized::{add_constants_and_apply_inv_sbox, add_constants_and_apply_sbox};

+ 48
- 42
src/hash/rescue/rpo/mod.rs

@ -4,7 +4,7 @@ use super::{
add_constants, add_constants_and_apply_inv_sbox, add_constants_and_apply_sbox, apply_inv_sbox,
apply_mds, apply_sbox, Digest, ElementHasher, Felt, FieldElement, Hasher, StarkField, ARK1,
ARK2, BINARY_CHUNK_SIZE, CAPACITY_RANGE, DIGEST_BYTES, DIGEST_RANGE, DIGEST_SIZE, INPUT1_RANGE,
INPUT2_RANGE, MDS, NUM_ROUNDS, ONE, RATE_RANGE, RATE_WIDTH, STATE_WIDTH, ZERO,
INPUT2_RANGE, MDS, NUM_ROUNDS, RATE_RANGE, RATE_WIDTH, STATE_WIDTH, ZERO,
};
mod digest;
@ -19,7 +19,8 @@ mod tests;
/// Implementation of the Rescue Prime Optimized hash function with 256-bit output.
///
/// The hash function is implemented according to the Rescue Prime Optimized
/// [specifications](https://eprint.iacr.org/2022/1577)
/// [specifications](https://eprint.iacr.org/2022/1577) while the padding rule follows the one
/// described [here](https://eprint.iacr.org/2023/1045).
///
/// The parameters used to instantiate the function are:
/// * Field: 64-bit prime field with modulus p = 2^64 - 2^32 + 1.
@ -51,7 +52,7 @@ mod tests;
///
/// Thus, if the underlying data consists of valid field elements, it might make more sense
/// to deserialize them into field elements and then hash them using
/// [hash_elements()](Rpo256::hash_elements) function rather then hashing the serialized bytes
/// [hash_elements()](Rpo256::hash_elements) function rather than hashing the serialized bytes
/// using [hash()](Rpo256::hash) function.
///
/// ## Domain separation
@ -64,6 +65,10 @@ mod tests;
/// becomes the bottleneck for the security bound of the sponge in overwrite-mode only when it is
/// lower than 2^128, we see that the target 128-bit security level is maintained as long as
/// the size of the domain identifier space, including for padding, is less than 2^128.
///
/// ## Hashing of empty input
/// The current implementation hashes empty input to the zero digest [0, 0, 0, 0]. This has
/// the benefit of requiring no calls to the RPO permutation when hashing empty input.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct Rpo256();
@ -77,14 +82,16 @@ impl Hasher for Rpo256 {
// initialize the state with zeroes
let mut state = [ZERO; STATE_WIDTH];
// set the capacity (first element) to a flag on whether or not the input length is evenly
// divided by the rate. this will prevent collisions between padded and non-padded inputs,
// and will rule out the need to perform an extra permutation in case of evenly divided
// inputs.
let is_rate_multiple = bytes.len() % RATE_WIDTH == 0;
if !is_rate_multiple {
state[CAPACITY_RANGE.start] = ONE;
}
// determine the number of field elements needed to encode `bytes` when each field element
// represents at most 7 bytes.
let num_field_elem = bytes.len().div_ceil(BINARY_CHUNK_SIZE);
// set the first capacity element to `RATE_WIDTH + (num_field_elem % RATE_WIDTH)`. We do
// this to achieve:
// 1. Domain separating hashing of `[u8]` from hashing of `[Felt]`.
// 2. Avoiding collisions at the `[Felt]` representation of the encoded bytes.
state[CAPACITY_RANGE.start] =
Felt::from((RATE_WIDTH + (num_field_elem % RATE_WIDTH)) as u8);
// initialize a buffer to receive the little-endian elements.
let mut buf = [0_u8; 8];
@ -93,41 +100,49 @@ impl Hasher for Rpo256 {
// into the state.
//
// every time the rate range is filled, a permutation is performed. if the final value of
// `i` is not zero, then the chunks count wasn't enough to fill the state range, and an
// additional permutation must be performed.
let i = bytes.chunks(BINARY_CHUNK_SIZE).fold(0, |i, chunk| {
// the last element of the iteration may or may not be a full chunk. if it's not, then
// we need to pad the remainder bytes of the chunk with zeroes, separated by a `1`.
// this will avoid collisions.
if chunk.len() == BINARY_CHUNK_SIZE {
// `rate_pos` is not zero, then the chunks count wasn't enough to fill the state range,
// and an additional permutation must be performed.
let mut current_chunk_idx = 0_usize;
// handle the case of an empty `bytes`
let last_chunk_idx = if num_field_elem == 0 {
current_chunk_idx
} else {
num_field_elem - 1
};
let rate_pos = bytes.chunks(BINARY_CHUNK_SIZE).fold(0, |rate_pos, chunk| {
// copy the chunk into the buffer
if current_chunk_idx != last_chunk_idx {
buf[..BINARY_CHUNK_SIZE].copy_from_slice(chunk);
} else {
// on the last iteration, we pad `buf` with a 1 followed by as many 0's as are
// needed to fill it
buf.fill(0);
buf[..chunk.len()].copy_from_slice(chunk);
buf[chunk.len()] = 1;
}
current_chunk_idx += 1;
// set the current rate element to the input. since we take at most 7 bytes, we are
// guaranteed that the inputs data will fit into a single field element.
state[RATE_RANGE.start + i] = Felt::new(u64::from_le_bytes(buf));
state[RATE_RANGE.start + rate_pos] = Felt::new(u64::from_le_bytes(buf));
// proceed filling the range. if it's full, then we apply a permutation and reset the
// counter to the beginning of the range.
if i == RATE_WIDTH - 1 {
if rate_pos == RATE_WIDTH - 1 {
Self::apply_permutation(&mut state);
0
} else {
i + 1
rate_pos + 1
}
});
// if we absorbed some elements but didn't apply a permutation to them (would happen when
// the number of elements is not a multiple of RATE_WIDTH), apply the RPO permutation. we
// don't need to apply any extra padding because the first capacity element contains a
// flag indicating whether the input is evenly divisible by the rate.
if i != 0 {
state[RATE_RANGE.start + i..RATE_RANGE.end].fill(ZERO);
state[RATE_RANGE.start + i] = ONE;
// flag indicating the number of field elements constituting the last block when the latter
// is not divisible by `RATE_WIDTH`.
if rate_pos != 0 {
state[RATE_RANGE.start + rate_pos..RATE_RANGE.end].fill(ZERO);
Self::apply_permutation(&mut state);
}
@ -153,24 +168,20 @@ impl Hasher for Rpo256 {
// initialize the state as follows:
// - seed is copied into the first 4 elements of the rate portion of the state.
// - if the value fits into a single field element, copy it into the fifth rate element and
// set the sixth rate element to 1.
// set the first capacity element to 5.
// - if the value doesn't fit into a single field element, split it into two field elements,
// copy them into rate elements 5 and 6, and set the seventh rate element to 1.
// - set the first capacity element to 1
// copy them into rate elements 5 and 6 and set the first capacity element to 6.
let mut state = [ZERO; STATE_WIDTH];
state[INPUT1_RANGE].copy_from_slice(seed.as_elements());
state[INPUT2_RANGE.start] = Felt::new(value);
if value < Felt::MODULUS {
state[INPUT2_RANGE.start + 1] = ONE;
state[CAPACITY_RANGE.start] = Felt::from(5_u8);
} else {
state[INPUT2_RANGE.start + 1] = Felt::new(value / Felt::MODULUS);
state[INPUT2_RANGE.start + 2] = ONE;
state[CAPACITY_RANGE.start] = Felt::from(6_u8);
}
// common padding for both cases
state[CAPACITY_RANGE.start] = ONE;
// apply the RPO permutation and return the first four elements of the state
// apply the RPO permutation and return the first four elements of the rate
Self::apply_permutation(&mut state);
RpoDigest::new(state[DIGEST_RANGE].try_into().unwrap())
}
@ -184,11 +195,9 @@ impl ElementHasher for Rpo256 {
let elements = E::slice_as_base_elements(elements);
// initialize state to all zeros, except for the first element of the capacity part, which
// is set to 1 if the number of elements is not a multiple of RATE_WIDTH.
// is set to `elements.len() % RATE_WIDTH`.
let mut state = [ZERO; STATE_WIDTH];
if elements.len() % RATE_WIDTH != 0 {
state[CAPACITY_RANGE.start] = ONE;
}
state[CAPACITY_RANGE.start] = Self::BaseField::from((elements.len() % RATE_WIDTH) as u8);
// absorb elements into the state one by one until the rate portion of the state is filled
// up; then apply the Rescue permutation and start absorbing again; repeat until all
@ -205,11 +214,8 @@ impl ElementHasher for Rpo256 {
// if we absorbed some elements but didn't apply a permutation to them (would happen when
// the number of elements is not a multiple of RATE_WIDTH), apply the RPO permutation after
// padding by appending a 1 followed by as many 0 as necessary to make the input length a
// multiple of the RATE_WIDTH.
// padding by as many 0 as necessary to make the input length a multiple of the RATE_WIDTH.
if i > 0 {
state[RATE_RANGE.start + i] = ONE;
i += 1;
while i != RATE_WIDTH {
state[RATE_RANGE.start + i] = ZERO;
i += 1;

+ 112
- 70
src/hash/rescue/rpo/tests.rs

@ -5,9 +5,12 @@ use rand_utils::rand_value;
use super::{
super::{apply_inv_sbox, apply_sbox, ALPHA, INV_ALPHA},
Felt, FieldElement, Hasher, Rpo256, RpoDigest, StarkField, ONE, STATE_WIDTH, ZERO,
Felt, FieldElement, Hasher, Rpo256, RpoDigest, StarkField, STATE_WIDTH, ZERO,
};
use crate::{
hash::rescue::{BINARY_CHUNK_SIZE, CAPACITY_RANGE, RATE_WIDTH},
Word, ONE,
};
use crate::Word;
#[test]
fn test_sbox() {
@ -126,6 +129,27 @@ fn hash_padding() {
assert_ne!(r1, r2);
}
#[test]
fn hash_padding_no_extra_permutation_call() {
use crate::hash::rescue::DIGEST_RANGE;
// Implementation
let num_bytes = BINARY_CHUNK_SIZE * RATE_WIDTH;
let mut buffer = vec![0_u8; num_bytes];
*buffer.last_mut().unwrap() = 97;
let r1 = Rpo256::hash(&buffer);
// Expected
let final_chunk = [0_u8, 0, 0, 0, 0, 0, 97, 1];
let mut state = [ZERO; STATE_WIDTH];
// padding when hashing bytes
state[CAPACITY_RANGE.start] = Felt::from(RATE_WIDTH as u8);
*state.last_mut().unwrap() = Felt::new(u64::from_le_bytes(final_chunk));
Rpo256::apply_permutation(&mut state);
assert_eq!(&r1[0..4], &state[DIGEST_RANGE]);
}
#[test]
fn hash_elements_padding() {
let e1 = [Felt::new(rand_value()); 2];
@ -159,6 +183,24 @@ fn hash_elements() {
assert_eq!(m_result, h_result);
}
#[test]
fn hash_empty() {
let elements: Vec<Felt> = vec![];
let zero_digest = RpoDigest::default();
let h_result = Rpo256::hash_elements(&elements);
assert_eq!(zero_digest, h_result);
}
#[test]
fn hash_empty_bytes() {
let bytes: Vec<u8> = vec![];
let zero_digest = RpoDigest::default();
let h_result = Rpo256::hash(&bytes);
assert_eq!(zero_digest, h_result);
}
#[test]
fn hash_test_vectors() {
let elements = [
@ -229,46 +271,46 @@ proptest! {
const EXPECTED: [Word; 19] = [
[
Felt::new(1502364727743950833),
Felt::new(5880949717274681448),
Felt::new(162790463902224431),
Felt::new(6901340476773664264),
Felt::new(18126731724905382595),
Felt::new(7388557040857728717),
Felt::new(14290750514634285295),
Felt::new(7852282086160480146),
],
[
Felt::new(7478710183745780580),
Felt::new(3308077307559720969),
Felt::new(3383561985796182409),
Felt::new(17205078494700259815),
Felt::new(10139303045932500183),
Felt::new(2293916558361785533),
Felt::new(15496361415980502047),
Felt::new(17904948502382283940),
],
[
Felt::new(17439912364295172999),
Felt::new(17979156346142712171),
Felt::new(8280795511427637894),
Felt::new(9349844417834368814),
Felt::new(17457546260239634015),
Felt::new(803990662839494686),
Felt::new(10386005777401424878),
Felt::new(18168807883298448638),
],
[
Felt::new(5105868198472766874),
Felt::new(13090564195691924742),
Felt::new(1058904296915798891),
Felt::new(18379501748825152268),
Felt::new(13072499238647455740),
Felt::new(10174350003422057273),
Felt::new(9201651627651151113),
Felt::new(6872461887313298746),
],
[
Felt::new(9133662113608941286),
Felt::new(12096627591905525991),
Felt::new(14963426595993304047),
Felt::new(13290205840019973377),
Felt::new(2903803350580990546),
Felt::new(1838870750730563299),
Felt::new(4258619137315479708),
Felt::new(17334260395129062936),
],
[
Felt::new(3134262397541159485),
Felt::new(10106105871979362399),
Felt::new(138768814855329459),
Felt::new(15044809212457404677),
Felt::new(8571221005243425262),
Felt::new(3016595589318175865),
Felt::new(13933674291329928438),
Felt::new(678640375034313072),
],
[
Felt::new(162696376578462826),
Felt::new(4991300494838863586),
Felt::new(660346084748120605),
Felt::new(13179389528641752698),
Felt::new(16314113978986502310),
Felt::new(14587622368743051587),
Felt::new(2808708361436818462),
Felt::new(10660517522478329440),
],
[
Felt::new(2242391899857912644),
@ -277,46 +319,46 @@ const EXPECTED: [Word; 19] = [
Felt::new(5046143039268215739),
],
[
Felt::new(9585630502158073976),
Felt::new(1310051013427303477),
Felt::new(7491921222636097758),
Felt::new(9417501558995216762),
Felt::new(5218076004221736204),
Felt::new(17169400568680971304),
Felt::new(8840075572473868990),
Felt::new(12382372614369863623),
],
[
Felt::new(1994394001720334744),
Felt::new(10866209900885216467),
Felt::new(13836092831163031683),
Felt::new(10814636682252756697),
Felt::new(9783834557155203486),
Felt::new(12317263104955018849),
Felt::new(3933748931816109604),
Felt::new(1843043029836917214),
],
[
Felt::new(17486854790732826405),
Felt::new(17376549265955727562),
Felt::new(2371059831956435003),
Felt::new(17585704935858006533),
Felt::new(14498234468286984551),
Felt::new(16837257669834682387),
Felt::new(6664141123711355107),
Felt::new(4590460158294697186),
],
[
Felt::new(11368277489137713825),
Felt::new(3906270146963049287),
Felt::new(10236262408213059745),
Felt::new(78552867005814007),
Felt::new(4661800562479916067),
Felt::new(11794407552792839953),
Felt::new(9037742258721863712),
Felt::new(6287820818064278819),
],
[
Felt::new(17899847381280262181),
Felt::new(14717912805498651446),
Felt::new(10769146203951775298),
Felt::new(2774289833490417856),
Felt::new(7752693085194633729),
Felt::new(7379857372245835536),
Felt::new(9270229380648024178),
Felt::new(10638301488452560378),
],
[
Felt::new(3794717687462954368),
Felt::new(4386865643074822822),
Felt::new(8854162840275334305),
Felt::new(7129983987107225269),
Felt::new(11542686762698783357),
Felt::new(15570714990728449027),
Felt::new(7518801014067819501),
Felt::new(12706437751337583515),
],
[
Felt::new(7244773535611633983),
Felt::new(19359923075859320),
Felt::new(10898655967774994333),
Felt::new(9319339563065736480),
Felt::new(9553923701032839042),
Felt::new(7281190920209838818),
Felt::new(2488477917448393955),
Felt::new(5088955350303368837),
],
[
Felt::new(4935426252518736883),
@ -325,21 +367,21 @@ const EXPECTED: [Word; 19] = [
Felt::new(18159875708229758073),
],
[
Felt::new(14871230873837295931),
Felt::new(11225255908868362971),
Felt::new(18100987641405432308),
Felt::new(1559244340089644233),
Felt::new(12795429638314178838),
Felt::new(14360248269767567855),
Felt::new(3819563852436765058),
Felt::new(10859123583999067291),
],
[
Felt::new(8348203744950016968),
Felt::new(4041411241960726733),
Felt::new(17584743399305468057),
Felt::new(16836952610803537051),
Felt::new(2695742617679420093),
Felt::new(9151515850666059759),
Felt::new(15855828029180595485),
Felt::new(17190029785471463210),
],
[
Felt::new(16139797453633030050),
Felt::new(1090233424040889412),
Felt::new(10770255347785669036),
Felt::new(16982398877290254028),
Felt::new(13205273108219124830),
Felt::new(2524898486192849221),
Felt::new(14618764355375283547),
Felt::new(10615614265042186874),
],
];

+ 26
- 11
src/hash/rescue/rpx/mod.rs

@ -11,6 +11,9 @@ use super::{
mod digest;
pub use digest::{RpxDigest, RpxDigestError};
#[cfg(test)]
mod tests;
pub type CubicExtElement = CubeExtension<Felt>;
// HASHER IMPLEMENTATION
@ -55,7 +58,7 @@ pub type CubicExtElement = CubeExtension;
///
/// Thus, if the underlying data consists of valid field elements, it might make more sense
/// to deserialize them into field elements and then hash them using
/// [hash_elements()](Rpx256::hash_elements) function rather then hashing the serialized bytes
/// [hash_elements()](Rpx256::hash_elements) function rather than hashing the serialized bytes
/// using [hash()](Rpx256::hash) function.
///
/// ## Domain separation
@ -68,6 +71,10 @@ pub type CubicExtElement = CubeExtension;
/// the bottleneck for the security bound of the sponge in overwrite-mode only when it is
/// lower than 2^128, we see that the target 128-bit security level is maintained as long as
/// the size of the domain identifier space, including for padding, is less than 2^128.
///
/// ## Hashing of empty input
/// The current implementation hashes empty input to the zero digest [0, 0, 0, 0]. This has
/// the benefit of requiring no calls to the RPX permutation when hashing empty input.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct Rpx256();
@ -99,11 +106,18 @@ impl Hasher for Rpx256 {
// into the state.
//
// every time the rate range is filled, a permutation is performed. if the final value of
// `i` is not zero, then the chunks count wasn't enough to fill the state range, and an
// additional permutation must be performed.
let i = bytes.chunks(BINARY_CHUNK_SIZE).fold(0, |i, chunk| {
// `rate_pos` is not zero, then the chunks count wasn't enough to fill the state range,
// and an additional permutation must be performed.
let mut current_chunk_idx = 0_usize;
// handle the case of an empty `bytes`
let last_chunk_idx = if num_field_elem == 0 {
current_chunk_idx
} else {
num_field_elem - 1
};
let rate_pos = bytes.chunks(BINARY_CHUNK_SIZE).fold(0, |rate_pos, chunk| {
// copy the chunk into the buffer
if i != num_field_elem - 1 {
if current_chunk_idx != last_chunk_idx {
buf[..BINARY_CHUNK_SIZE].copy_from_slice(chunk);
} else {
// on the last iteration, we pad `buf` with a 1 followed by as many 0's as are
@ -112,18 +126,19 @@ impl Hasher for Rpx256 {
buf[..chunk.len()].copy_from_slice(chunk);
buf[chunk.len()] = 1;
}
current_chunk_idx += 1;
// set the current rate element to the input. since we take at most 7 bytes, we are
// guaranteed that the inputs data will fit into a single field element.
state[RATE_RANGE.start + i] = Felt::new(u64::from_le_bytes(buf));
state[RATE_RANGE.start + rate_pos] = Felt::new(u64::from_le_bytes(buf));
// proceed filling the range. if it's full, then we apply a permutation and reset the
// counter to the beginning of the range.
if i == RATE_WIDTH - 1 {
if rate_pos == RATE_WIDTH - 1 {
Self::apply_permutation(&mut state);
0
} else {
i + 1
rate_pos + 1
}
});
@ -132,8 +147,8 @@ impl Hasher for Rpx256 {
// don't need to apply any extra padding because the first capacity element contains a
// flag indicating the number of field elements constituting the last block when the latter
// is not divisible by `RATE_WIDTH`.
if i != 0 {
state[RATE_RANGE.start + i..RATE_RANGE.end].fill(ZERO);
if rate_pos != 0 {
state[RATE_RANGE.start + rate_pos..RATE_RANGE.end].fill(ZERO);
Self::apply_permutation(&mut state);
}
@ -172,7 +187,7 @@ impl Hasher for Rpx256 {
state[CAPACITY_RANGE.start] = Felt::from(6_u8);
}
// apply the RPX permutation and return the first four elements of the state
// apply the RPX permutation and return the first four elements of the rate
Self::apply_permutation(&mut state);
RpxDigest::new(state[DIGEST_RANGE].try_into().unwrap())
}

+ 186
- 0
src/hash/rescue/rpx/tests.rs

@ -0,0 +1,186 @@
use alloc::{collections::BTreeSet, vec::Vec};
use proptest::prelude::*;
use rand_utils::rand_value;
use super::{Felt, Hasher, Rpx256, StarkField, ZERO};
use crate::{hash::rescue::RpxDigest, ONE};
#[test]
fn hash_elements_vs_merge() {
let elements = [Felt::new(rand_value()); 8];
let digests: [RpxDigest; 2] = [
RpxDigest::new(elements[..4].try_into().unwrap()),
RpxDigest::new(elements[4..].try_into().unwrap()),
];
let m_result = Rpx256::merge(&digests);
let h_result = Rpx256::hash_elements(&elements);
assert_eq!(m_result, h_result);
}
#[test]
fn merge_vs_merge_in_domain() {
let elements = [Felt::new(rand_value()); 8];
let digests: [RpxDigest; 2] = [
RpxDigest::new(elements[..4].try_into().unwrap()),
RpxDigest::new(elements[4..].try_into().unwrap()),
];
let merge_result = Rpx256::merge(&digests);
// ----- merge with domain = 0 ----------------------------------------------------------------
// set domain to ZERO. This should not change the result.
let domain = ZERO;
let merge_in_domain_result = Rpx256::merge_in_domain(&digests, domain);
assert_eq!(merge_result, merge_in_domain_result);
// ----- merge with domain = 1 ----------------------------------------------------------------
// set domain to ONE. This should change the result.
let domain = ONE;
let merge_in_domain_result = Rpx256::merge_in_domain(&digests, domain);
assert_ne!(merge_result, merge_in_domain_result);
}
#[test]
fn hash_elements_vs_merge_with_int() {
let tmp = [Felt::new(rand_value()); 4];
let seed = RpxDigest::new(tmp);
// ----- value fits into a field element ------------------------------------------------------
let val: Felt = Felt::new(rand_value());
let m_result = Rpx256::merge_with_int(seed, val.as_int());
let mut elements = seed.as_elements().to_vec();
elements.push(val);
let h_result = Rpx256::hash_elements(&elements);
assert_eq!(m_result, h_result);
// ----- value does not fit into a field element ----------------------------------------------
let val = Felt::MODULUS + 2;
let m_result = Rpx256::merge_with_int(seed, val);
let mut elements = seed.as_elements().to_vec();
elements.push(Felt::new(val));
elements.push(ONE);
let h_result = Rpx256::hash_elements(&elements);
assert_eq!(m_result, h_result);
}
#[test]
fn hash_padding() {
// adding a zero bytes at the end of a byte string should result in a different hash
let r1 = Rpx256::hash(&[1_u8, 2, 3]);
let r2 = Rpx256::hash(&[1_u8, 2, 3, 0]);
assert_ne!(r1, r2);
// same as above but with bigger inputs
let r1 = Rpx256::hash(&[1_u8, 2, 3, 4, 5, 6]);
let r2 = Rpx256::hash(&[1_u8, 2, 3, 4, 5, 6, 0]);
assert_ne!(r1, r2);
// same as above but with input splitting over two elements
let r1 = Rpx256::hash(&[1_u8, 2, 3, 4, 5, 6, 7]);
let r2 = Rpx256::hash(&[1_u8, 2, 3, 4, 5, 6, 7, 0]);
assert_ne!(r1, r2);
// same as above but with multiple zeros
let r1 = Rpx256::hash(&[1_u8, 2, 3, 4, 5, 6, 7, 0, 0]);
let r2 = Rpx256::hash(&[1_u8, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0]);
assert_ne!(r1, r2);
}
#[test]
fn hash_elements_padding() {
let e1 = [Felt::new(rand_value()); 2];
let e2 = [e1[0], e1[1], ZERO];
let r1 = Rpx256::hash_elements(&e1);
let r2 = Rpx256::hash_elements(&e2);
assert_ne!(r1, r2);
}
#[test]
fn hash_elements() {
let elements = [
ZERO,
ONE,
Felt::new(2),
Felt::new(3),
Felt::new(4),
Felt::new(5),
Felt::new(6),
Felt::new(7),
];
let digests: [RpxDigest; 2] = [
RpxDigest::new(elements[..4].try_into().unwrap()),
RpxDigest::new(elements[4..8].try_into().unwrap()),
];
let m_result = Rpx256::merge(&digests);
let h_result = Rpx256::hash_elements(&elements);
assert_eq!(m_result, h_result);
}
#[test]
fn hash_empty() {
let elements: Vec<Felt> = vec![];
let zero_digest = RpxDigest::default();
let h_result = Rpx256::hash_elements(&elements);
assert_eq!(zero_digest, h_result);
}
#[test]
fn hash_empty_bytes() {
let bytes: Vec<u8> = vec![];
let zero_digest = RpxDigest::default();
let h_result = Rpx256::hash(&bytes);
assert_eq!(zero_digest, h_result);
}
#[test]
fn sponge_bytes_with_remainder_length_wont_panic() {
// this test targets to assert that no panic will happen with the edge case of having an inputs
// with length that is not divisible by the used binary chunk size. 113 is a non-negligible
// input length that is prime; hence guaranteed to not be divisible by any choice of chunk
// size.
//
// this is a preliminary test to the fuzzy-stress of proptest.
Rpx256::hash(&[0; 113]);
}
#[test]
fn sponge_collision_for_wrapped_field_element() {
let a = Rpx256::hash(&[0; 8]);
let b = Rpx256::hash(&Felt::MODULUS.to_le_bytes());
assert_ne!(a, b);
}
#[test]
fn sponge_zeroes_collision() {
let mut zeroes = Vec::with_capacity(255);
let mut set = BTreeSet::new();
(0..255).for_each(|_| {
let hash = Rpx256::hash(&zeroes);
zeroes.push(0);
// panic if a collision was found
assert!(set.insert(hash));
});
}
proptest! {
#[test]
fn rpo256_wont_panic_with_arbitrary_input(ref bytes in any::<Vec<u8>>()) {
Rpx256::hash(bytes);
}
}

Loading…
Cancel
Save