diff --git a/CHANGELOG.md b/CHANGELOG.md index acc6a6b..527edaa 100644 --- a/CHANGELOG.md +++ b/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) diff --git a/src/dsa/rpo_falcon512/keys/secret_key.rs b/src/dsa/rpo_falcon512/keys/secret_key.rs index 1cf1d90..0d4b709 100644 --- a/src/dsa/rpo_falcon512/keys/secret_key.rs +++ b/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 ϕ] diff --git a/src/hash/mod.rs b/src/hash/mod.rs index 630a6f5..e7fd9c7 100644 --- a/src/hash/mod.rs +++ b/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; diff --git a/src/hash/rescue/mod.rs b/src/hash/rescue/mod.rs index b22c111..fee20ab 100644 --- a/src/hash/rescue/mod.rs +++ b/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}; diff --git a/src/hash/rescue/rpo/mod.rs b/src/hash/rescue/rpo/mod.rs index 99fac78..0d1d87b 100644 --- a/src/hash/rescue/rpo/mod.rs +++ b/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; diff --git a/src/hash/rescue/rpo/tests.rs b/src/hash/rescue/rpo/tests.rs index c43a14d..934c18f 100644 --- a/src/hash/rescue/rpo/tests.rs +++ b/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 = 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 = 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), ], ]; diff --git a/src/hash/rescue/rpx/mod.rs b/src/hash/rescue/rpx/mod.rs index 7296733..8c6c53b 100644 --- a/src/hash/rescue/rpx/mod.rs +++ b/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; // 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()) } diff --git a/src/hash/rescue/rpx/tests.rs b/src/hash/rescue/rpx/tests.rs new file mode 100644 index 0000000..e8087f6 --- /dev/null +++ b/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 = 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 = 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::>()) { + Rpx256::hash(bytes); + } +}