diff --git a/CHANGELOG.md b/CHANGELOG.md index b080df27d..66c08b25e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - [BREAKING] Fix OOMs in Merkle/SMT deserialization ([#820](https://github.com/0xMiden/crypto/pull/820)). - Fixed `SmtForest` to remove nodes with zero reference count from store ([#821](https://github.com/0xMiden/crypto/pull/821)). - Cross-checked RPO test vectors against the Python reference implementation after state layout change ([#822](https://github.com/0xMiden/crypto/pull/822)). +- Added `hash_iter()` to `Rpo256`, `Rpx256`, and `Poseidon2` for hashing field elements from iterators without allocation ([#823](https://github.com/0xMiden/crypto/pull/823)). - Fixed tuple `min_serialized_size()` to exclude alignment padding, fixing `BudgetedReader` rejecting valid data ([#827](https://github.com/0xMiden/crypto/pull/827)). ## 0.22.2 (2026-02-01) diff --git a/miden-crypto/src/hash/algebraic_sponge/mod.rs b/miden-crypto/src/hash/algebraic_sponge/mod.rs index 30f83bc8d..481b71638 100644 --- a/miden-crypto/src/hash/algebraic_sponge/mod.rs +++ b/miden-crypto/src/hash/algebraic_sponge/mod.rs @@ -99,6 +99,50 @@ pub(crate) trait AlgebraicSponge { Word::new(state[DIGEST_RANGE].try_into().unwrap()) } + /// Returns a hash of field elements provided via an iterator. + /// + /// This is functionally equivalent to [hash_elements()](Self::hash_elements) but avoids + /// requiring a contiguous slice, which can eliminate intermediate allocations when the + /// elements are produced lazily or come from multiple sources. + /// + /// The iterator must implement [ExactSizeIterator] because the total element count is needed + /// upfront for domain separation. + fn hash_iter(iter: I) -> Word + where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + { + let iter = iter.into_iter(); + let total_len = iter.len(); + + if total_len == 0 { + return Word::default(); + } + + let mut state = [ZERO; STATE_WIDTH]; + state[CAPACITY_RANGE.start] = Felt::from_u8((total_len % RATE_WIDTH) as u8); + + let mut i = 0; + for felt in iter { + state[RATE_RANGE.start + i] = felt; + i += 1; + if i == RATE_WIDTH { + Self::apply_permutation(&mut state); + i = 0; + } + } + + if i > 0 { + while i != RATE_WIDTH { + state[RATE_RANGE.start + i] = ZERO; + i += 1; + } + Self::apply_permutation(&mut state); + } + + Word::new(state[DIGEST_RANGE].try_into().unwrap()) + } + /// Returns a hash of the provided sequence of bytes. fn hash(bytes: &[u8]) -> Word { // initialize the state with zeroes diff --git a/miden-crypto/src/hash/algebraic_sponge/poseidon2/mod.rs b/miden-crypto/src/hash/algebraic_sponge/poseidon2/mod.rs index 6152f27c4..5a121eadb 100644 --- a/miden-crypto/src/hash/algebraic_sponge/poseidon2/mod.rs +++ b/miden-crypto/src/hash/algebraic_sponge/poseidon2/mod.rs @@ -151,6 +151,19 @@ impl Poseidon2 { ::hash_elements(elements) } + /// Returns a hash of field elements provided via an iterator. + /// + /// This is functionally equivalent to [hash_elements()](Self::hash_elements) but avoids + /// requiring a contiguous slice, which can eliminate intermediate allocations. + #[inline(always)] + pub fn hash_iter(iter: I) -> Word + where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + { + ::hash_iter(iter) + } + /// Returns a hash of two digests. This method is intended for use in construction of /// Merkle trees and verification of Merkle paths. #[inline(always)] diff --git a/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/mod.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/mod.rs index f122f05b3..e31a35cf7 100644 --- a/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/mod.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/mod.rs @@ -135,6 +135,19 @@ impl Rpo256 { ::hash_elements(elements) } + /// Returns a hash of field elements provided via an iterator. + /// + /// This is functionally equivalent to [hash_elements()](Self::hash_elements) but avoids + /// requiring a contiguous slice, which can eliminate intermediate allocations. + #[inline(always)] + pub fn hash_iter(iter: I) -> Word + where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + { + ::hash_iter(iter) + } + /// Returns a hash of two digests. This method is intended for use in construction of /// Merkle trees and verification of Merkle paths. #[inline(always)] diff --git a/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs index 84e30a4c3..38f82b82a 100644 --- a/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs @@ -211,6 +211,38 @@ fn hash_empty_bytes() { assert_eq!(zero_digest, h_result); } +#[test] +fn hash_iter_vs_hash_elements() { + // empty input + let empty: Vec = vec![]; + assert_eq!(Rpo256::hash_iter(empty.iter().copied()), Rpo256::hash_elements(&empty)); + + // single element + let single = [Felt::new(42)]; + assert_eq!(Rpo256::hash_iter(single.iter().copied()), Rpo256::hash_elements(&single)); + + // partial rate (less than 8 elements) + let partial: Vec = (0..5).map(Felt::new).collect(); + assert_eq!(Rpo256::hash_iter(partial.iter().copied()), Rpo256::hash_elements(&partial)); + + // exact rate (exactly 8 elements) + let exact_rate: Vec = (0..8).map(Felt::new).collect(); + assert_eq!( + Rpo256::hash_iter(exact_rate.iter().copied()), + Rpo256::hash_elements(&exact_rate) + ); + + // multiple rates (more than 8 elements) + let multi: Vec = (0..19).map(Felt::new).collect(); + assert_eq!(Rpo256::hash_iter(multi.iter().copied()), Rpo256::hash_elements(&multi)); + + // verify against known test vectors + for (i, expected) in EXPECTED.iter().enumerate() { + let elements: Vec = (0..=i).map(|j| Felt::new(j as u64)).collect(); + assert_eq!(Rpo256::hash_iter(elements.iter().copied()), *expected); + } +} + #[test] fn hash_test_vectors() { let elements = [ diff --git a/miden-crypto/src/hash/algebraic_sponge/rescue/rpx/mod.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/rpx/mod.rs index 6e48f89e1..290ce647c 100644 --- a/miden-crypto/src/hash/algebraic_sponge/rescue/rpx/mod.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/rpx/mod.rs @@ -136,6 +136,19 @@ impl Rpx256 { ::hash_elements(elements) } + /// Returns a hash of field elements provided via an iterator. + /// + /// This is functionally equivalent to [hash_elements()](Self::hash_elements) but avoids + /// requiring a contiguous slice, which can eliminate intermediate allocations. + #[inline(always)] + pub fn hash_iter(iter: I) -> Word + where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + { + ::hash_iter(iter) + } + /// Returns a hash of two digests. This method is intended for use in construction of /// Merkle trees and verification of Merkle paths. #[inline(always)]