Skip to content
This repository was archived by the owner on Aug 7, 2026. It is now read-only.
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
## 0.22.0 (TBD)

- Added `MmrPath::with_forest()` and `MmrProof::with_forest()` to adjust proofs for smaller forests ([#788](https://github.com/0xMiden/crypto/pull/788)).
-
- Added const-generic `Digest<N>` struct for binary hash functions with `Digest256` and `Digest512` type aliases ([#777](https://github.com/0xMiden/crypto/pull/777)).

## 0.21.4 (2026-01-22)

- Fix an issue where `BudgetedReader` rejects valid usize collections with tight budgets ([#798](https://github.com/0xMiden/crypto/pull/798)).
Expand Down
25 changes: 3 additions & 22 deletions miden-crypto/src/hash/blake/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
use alloc::string::String;
use core::{
mem::size_of,
ops::Deref,
slice::{self, from_raw_parts},
};
use core::{mem::size_of, ops::Deref, slice};

use p3_field::{BasedVectorSpace, PrimeField64};
use p3_goldilocks::Goldilocks as Felt;
Expand Down Expand Up @@ -133,7 +129,7 @@ impl Blake3_256 {
// (<Self as Hasher>::merge). They're now direct implementations as part of removing
// the Winterfell Hasher trait dependency. These are public API used in benchmarks.
pub fn merge(values: &[Blake3Digest<32>; 2]) -> Blake3Digest<32> {
Self::hash(prepare_merge(values))
Self::hash(Blake3Digest::digests_as_bytes(values))
}

pub fn merge_many(values: &[Blake3Digest<32>]) -> Blake3Digest<32> {
Expand Down Expand Up @@ -194,7 +190,7 @@ impl Blake3_192 {
}

pub fn merge(values: &[Blake3Digest<24>; 2]) -> Blake3Digest<24> {
Self::hash(prepare_merge(values))
Self::hash(Blake3Digest::digests_as_bytes(values))
}

pub fn merge_with_int(seed: Blake3Digest<24>, value: u64) -> Blake3Digest<24> {
Expand Down Expand Up @@ -275,18 +271,3 @@ fn expand_bytes<const M: usize, const N: usize>(bytes: &[u8; M]) -> [u8; N] {
expanded[..M].copy_from_slice(bytes);
expanded
}

// Cast the slice into contiguous bytes.
fn prepare_merge<const N: usize, D>(args: &[D; N]) -> &[u8]
where
D: Deref<Target = [u8]>,
{
// compile-time assertion
assert!(N > 0, "N shouldn't represent an empty slice!");
let values = args.as_ptr() as *const u8;
let len = size_of::<D>() * N;
// safety: the values are tested to be contiguous
let bytes = unsafe { from_raw_parts(values, len) };
debug_assert_eq!(args[0].deref(), &bytes[..len / N]);
bytes
}
222 changes: 222 additions & 0 deletions miden-crypto/src/hash/digest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
//! Generic digest types for binary hash functions.
//!
//! This module provides a reusable const-generic digest struct for hash functions with
//! fixed-size outputs. The default size is 32 bytes (256 bits), suitable for SHA-256,
//! Blake3-256, etc. For 64-byte outputs (e.g., SHA-512), use `Digest<64>`.

use alloc::string::String;
use core::{ops::Deref, slice};

use crate::utils::{
ByteReader, ByteWriter, Deserializable, DeserializationError, HexParseError, Serializable,
bytes_to_hex_string, hex_to_bytes,
};

// CONSTANTS
// ================================================================================================

/// Size of a 256-bit digest in bytes.
pub const DIGEST256_BYTES: usize = 32;

/// Size of a 512-bit digest in bytes.
pub const DIGEST512_BYTES: usize = 64;

// TYPE ALIASES
// ================================================================================================

/// A 256-bit (32-byte) digest. Type alias for `Digest<32>`.
pub type Digest256 = Digest<DIGEST256_BYTES>;

/// A 512-bit (64-byte) digest. Type alias for `Digest<64>`.
pub type Digest512 = Digest<DIGEST512_BYTES>;

// DIGEST
// ================================================================================================

/// A fixed-size digest for binary hash functions.
///
/// This struct provides a generic, reusable digest type for hash functions that produce
/// fixed-size outputs. The const parameter `N` specifies the digest size in bytes,
/// defaulting to 32 bytes (256 bits).
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(into = "String", try_from = "&str"))]
#[repr(transparent)]
pub struct Digest<const N: usize = DIGEST256_BYTES>([u8; N]);

impl<const N: usize> Digest<N> {
/// Creates a new digest from the given bytes.
#[inline]
pub const fn new(bytes: [u8; N]) -> Self {
Self(bytes)
}

/// Returns the digest as a byte array reference.
#[inline]
pub fn as_bytes(&self) -> &[u8; N] {
&self.0
}

/// Converts a slice of digests into a contiguous byte slice.
pub fn digests_as_bytes(digests: &[Digest<N>]) -> &[u8] {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prepare_merge function is duplicated in sha2, keccak, and blake modules. Consider moving it here and exporting it so all three can share it.

let p = digests.as_ptr();
let len = digests.len() * N;
// SAFETY: Digest<N> is repr(transparent) over [u8; N], so this is safe
unsafe { slice::from_raw_parts(p as *const u8, len) }
}
}

impl<const N: usize> Default for Digest<N> {
fn default() -> Self {
Self([0; N])
}
}

impl<const N: usize> Deref for Digest<N> {
type Target = [u8];

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<const N: usize> From<Digest<N>> for [u8; N] {
fn from(value: Digest<N>) -> Self {
value.0
}
}

impl<const N: usize> From<[u8; N]> for Digest<N> {
fn from(value: [u8; N]) -> Self {
Self(value)
}
}

impl<const N: usize> From<Digest<N>> for String {
fn from(value: Digest<N>) -> Self {
bytes_to_hex_string(value.0)
}
}

impl<const N: usize> TryFrom<&str> for Digest<N> {
type Error = HexParseError;

fn try_from(value: &str) -> Result<Self, Self::Error> {
hex_to_bytes(value).map(Self)
}
}

impl<const N: usize> Serializable for Digest<N> {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write_bytes(&self.0);
}
}

impl<const N: usize> Deserializable for Digest<N> {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
source.read_array().map(Self)
}
}

// TESTS
// ================================================================================================

#[cfg(test)]
mod tests {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I'd add a test_memory_layout_assumptions test here to verify size_of and align_of match the inner array. The unsafe code in digests_as_bytes and prepare_merge relies on this.

use core::mem::{align_of, size_of};

use super::*;

#[test]
fn test_memory_layout_assumptions() {
// Verify that Digest<N> has the same size and alignment as [u8; N].
// The unsafe code in digests_as_bytes relies on this.
assert_eq!(size_of::<Digest<32>>(), size_of::<[u8; 32]>());
assert_eq!(align_of::<Digest<32>>(), align_of::<[u8; 32]>());

assert_eq!(size_of::<Digest<64>>(), size_of::<[u8; 64]>());
assert_eq!(align_of::<Digest<64>>(), align_of::<[u8; 64]>());

// Verify type aliases as well
assert_eq!(size_of::<Digest256>(), 32);
assert_eq!(size_of::<Digest512>(), 64);
}

#[test]
fn test_digest_default_32() {
let digest = Digest::<32>::default();
assert_eq!(digest.as_bytes(), &[0u8; 32]);
}

#[test]
fn test_digest_default_64() {
let digest = Digest::<64>::default();
assert_eq!(digest.as_bytes(), &[0u8; 64]);
}

#[test]
fn test_digest256_alias() {
let digest = Digest256::default();
assert_eq!(digest.as_bytes(), &[0u8; 32]);
}

#[test]
fn test_digest512_alias() {
let digest = Digest512::default();
assert_eq!(digest.as_bytes(), &[0u8; 64]);
}

#[test]
fn test_digest_from_bytes_32() {
let bytes = [1u8; 32];
let digest = Digest::<32>::from(bytes);
assert_eq!(digest.as_bytes(), &bytes);
}

#[test]
fn test_digest_from_bytes_64() {
let bytes = [1u8; 64];
let digest = Digest::<64>::from(bytes);
assert_eq!(digest.as_bytes(), &bytes);
}

#[test]
fn test_digest_hex_roundtrip_32() {
let bytes = [0xab; 32];
let digest = Digest::<32>::from(bytes);
let hex: String = digest.into();
let recovered = Digest::<32>::try_from(hex.as_str()).unwrap();
assert_eq!(recovered.as_bytes(), &bytes);
}

#[test]
fn test_digest_hex_roundtrip_64() {
let bytes = [0xcd; 64];
let digest = Digest::<64>::from(bytes);
let hex: String = digest.into();
let recovered = Digest::<64>::try_from(hex.as_str()).unwrap();
assert_eq!(recovered.as_bytes(), &bytes);
}

#[test]
fn test_digest_digests_as_bytes_32() {
let d1 = Digest::<32>::from([1u8; 32]);
let d2 = Digest::<32>::from([2u8; 32]);
let digests = [d1, d2];
let bytes = Digest::<32>::digests_as_bytes(&digests);
assert_eq!(bytes.len(), 64);
assert_eq!(&bytes[0..32], &[1u8; 32]);
assert_eq!(&bytes[32..64], &[2u8; 32]);
}

#[test]
fn test_digest_digests_as_bytes_64() {
let d1 = Digest::<64>::from([1u8; 64]);
let d2 = Digest::<64>::from([2u8; 64]);
let digests = [d1, d2];
let bytes = Digest::<64>::digests_as_bytes(&digests);
assert_eq!(bytes.len(), 128);
assert_eq!(&bytes[0..64], &[1u8; 64]);
assert_eq!(&bytes[64..128], &[2u8; 64]);
}
}
Loading