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 4 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 @@ -13,6 +13,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
205 changes: 205 additions & 0 deletions miden-crypto/src/hash/digest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
//! 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;
Comment thread
plafer marked this conversation as resolved.
Outdated

/// A 512-bit (64-byte) digest. Type alias for `Digest<64>`.
pub type Digest512 = Digest<64>;
Comment thread
plafer marked this conversation as resolved.
Outdated

// 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 = 32>([u8; N]);
Comment thread
plafer marked this conversation as resolved.
Outdated

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 super::*;

#[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]);
}
}
3 changes: 3 additions & 0 deletions miden-crypto/src/hash/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

use crate::{Felt, Word, ZERO};

/// Generic digest types for binary hash functions.
pub mod digest;
Comment thread
bobbinth marked this conversation as resolved.
Outdated

/// Blake3 hash function.
pub mod blake;

Expand Down
Loading