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 9 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- [BREAKING] Changed sponge state layout from `[CAPACITY, RATE1, RATE0]` (BE) to `[RATE0, RATE1, CAPACITY]` (LE) ([#755](https://github.com/0xMiden/crypto/pull/755)).
- [BREAKING] Added length-prefixing to Serializable/Deserializable impls for collections, fuzz deserialization for panics ([#757](https://github.com/0xMiden/crypto/pull/757)).
- Added `SmtLeaf::try_from_elements()` ([#773](https://github.com/0xMiden/crypto/pull/773)).
- Added const-generic `Digest<N>` struct for binary hash functions with `Digest256` and `Digest512` type aliases ([#777](https://github.com/0xMiden/crypto/pull/777)).
Comment thread
bobbinth marked this conversation as resolved.
Outdated
- Copied `WordWrapper` macro from `miden-base` to `miden-crypto-derive`.

# 0.20.1 (2025-12-29)
Expand Down
23 changes: 2 additions & 21 deletions miden-crypto/src/hash/blake/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
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;

use super::HasherExt;
use super::{HasherExt, digest::prepare_merge};
use crate::utils::{
ByteReader, ByteWriter, Deserializable, DeserializationError, HexParseError, Serializable,
bytes_to_hex_string, hex_to_bytes,
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
}
243 changes: 243 additions & 0 deletions miden-crypto/src/hash/digest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
//! 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::{mem::size_of, 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)
}
}

// HELPER FUNCTIONS
// ================================================================================================

/// Cast the slice into contiguous bytes.
///
/// This function is used by hash implementations to efficiently convert an array of digests
/// into a byte slice for hashing in merge operations.
pub fn prepare_merge<const N: usize, D>(args: &[D; N]) -> &[u8]
Comment thread
bobbinth marked this conversation as resolved.
Outdated
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 { slice::from_raw_parts(values, len) };
debug_assert_eq!(args[0].deref(), &bytes[..len / N]);
bytes
}

// 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 and prepare_merge 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