use curve25519_dalek::RistrettoPoint;
use dataview::Pod;
pub use digest;
use parking_lot::Mutex;
#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::marker::PhantomData;
use std::num::NonZeroUsize;
use thiserror::Error;
#[derive(Debug, Default)]
struct CommutativeHashBuilder<Hash>
where
Hash: digest::Digest<OutputSize = digest::consts::U64> + Default,
{
point: RistrettoPoint,
_marker: PhantomData<Hash>,
}
impl<Hash> CommutativeHashBuilder<Hash>
where
Hash: digest::Digest<OutputSize = digest::consts::U64> + Default,
{
fn add_hash(&mut self, hash: Hash) {
self.point += RistrettoPoint::from_hash(hash);
}
fn finalize(&self) -> CommutativeHashDigest {
CommutativeHashDigest(self.point.compress().to_bytes())
}
}
impl<Hash> std::ops::AddAssign for CommutativeHashBuilder<Hash>
where
Hash: digest::Digest<OutputSize = digest::consts::U64> + Default,
{
fn add_assign(&mut self, rhs: Self) {
self.point += rhs.point;
}
}
#[derive(Debug, Error)]
pub enum CommutativeHashDigestError {
#[error("Invalid character {:?} at position {}", c, index)]
InvalidHexCharacter { c: char, index: usize },
#[error("Odd number of characters")]
OddLength,
#[error("Invalid string length")]
InvalidStringLength,
#[error("Digest is not 32 bytes long")]
Not32Bytes,
}
#[derive(Copy, Clone, Ord, PartialOrd, PartialEq, Eq, Hash, Debug, Default, Pod)]
#[repr(C)]
pub struct CommutativeHashDigest([u8; 32]);
impl CommutativeHashDigest {
pub fn to_bytes(&self) -> [u8; 32] {
self.0
}
pub fn hex_digest(&self) -> String {
hex::encode(self.0)
}
pub fn decode_hex_digest(
hex_digest: &str,
) -> core::result::Result<Self, CommutativeHashDigestError> {
let value = hex::decode(hex_digest).map_err(|e| match e {
hex::FromHexError::InvalidHexCharacter { c, index } => {
CommutativeHashDigestError::InvalidHexCharacter { c, index }
}
hex::FromHexError::OddLength => CommutativeHashDigestError::OddLength,
hex::FromHexError::InvalidStringLength => {
CommutativeHashDigestError::InvalidStringLength
}
})?;
value
.try_into()
.map(Self)
.map_err(|_| CommutativeHashDigestError::Not32Bytes)
}
}
#[cfg(feature = "serde")]
impl Serialize for CommutativeHashDigest {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = self.hex_digest();
serializer.serialize_str(&s)
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for CommutativeHashDigest {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
CommutativeHashDigest::decode_hex_digest(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Error)]
pub enum ParallelHasherError {
#[error("Data is not aligned on a block boundary")]
MisalignedData,
#[error("Provided data is larger than possible")]
OutOfBounds,
}
pub type Result<T, E = ParallelHasherError> = std::result::Result<T, E>;
#[derive(Debug)]
pub struct ParallelHasher<Hash>
where
Hash: digest::Digest<OutputSize = digest::consts::U64> + Default,
{
inner: Mutex<CommutativeHashBuilder<Hash>>,
block_size: usize,
}
impl<Hash> ParallelHasher<Hash>
where
Hash: digest::Digest<OutputSize = digest::consts::U64> + Default,
{
pub fn new(block_size: NonZeroUsize) -> Self {
ParallelHasher {
inner: Mutex::new(CommutativeHashBuilder::default()),
block_size: block_size.get(),
}
}
pub fn update(&self, start_byte: usize, data: &[u8]) -> Result<()> {
if !start_byte.is_multiple_of(self.block_size) {
return Err(ParallelHasherError::MisalignedData);
}
if start_byte.checked_add(data.len()).is_none() {
return Err(ParallelHasherError::OutOfBounds);
};
let mut start = start_byte;
let chunk_hash = data.chunks(self.block_size).fold(
CommutativeHashBuilder::<Hash>::default(),
|mut acc, chunk| {
let len = chunk.len();
let mut hash = Hash::default();
hash.update(chunk);
hash.update(to_padded_bytes(start));
hash.update(to_padded_bytes(len));
start += len;
acc.add_hash(hash);
acc
},
);
let mut inner = self.inner.lock();
*inner += chunk_hash;
Ok(())
}
pub fn finalize(self) -> CommutativeHashDigest {
self.inner.lock().finalize()
}
}
#[derive(Debug)]
pub struct SequentialHasher<Hash>
where
Hash: digest::Digest<OutputSize = digest::consts::U64> + Default,
{
inner: CommutativeHashBuilder<Hash>,
scratch: Hash,
scratch_start: usize,
scratch_size: usize,
block_size: usize,
}
impl<Hash> SequentialHasher<Hash>
where
Hash: digest::Digest<OutputSize = digest::consts::U64> + Default,
{
pub fn new(block_size: NonZeroUsize) -> Self {
SequentialHasher {
inner: CommutativeHashBuilder::default(),
scratch: Hash::default(),
scratch_start: 0,
scratch_size: 0,
block_size: block_size.get(),
}
}
pub fn update(&mut self, data: &[u8]) {
let remaining = self.block_size - self.scratch_size;
split_first_then_chunks(data, remaining, self.block_size).for_each(|chunk| {
self.scratch.update(chunk);
self.scratch_size += chunk.len();
if self.scratch_size == self.block_size {
self.flush();
}
});
}
pub fn finalize(mut self) -> CommutativeHashDigest {
self.flush();
self.inner.finalize()
}
pub fn digest_from_bytes(block_size: NonZeroUsize, data: &[u8]) -> CommutativeHashDigest {
let mut hasher = Self::new(block_size);
hasher.update(data);
hasher.finalize()
}
fn flush(&mut self) {
if self.scratch_size == 0 {
return;
}
let mut scratch = std::mem::take(&mut self.scratch);
scratch.update(to_padded_bytes(self.scratch_start));
scratch.update(to_padded_bytes(self.scratch_size));
self.inner.add_hash(scratch);
self.scratch_start += self.scratch_size;
self.scratch_size = 0;
}
}
fn split_first_then_chunks(
data: &[u8],
first_size: usize,
block_size: usize,
) -> impl Iterator<Item = &[u8]> {
let (first, rest) = data.split_at(data.len().min(first_size));
std::iter::once(first).chain(rest.chunks(block_size))
}
fn to_padded_bytes(v: usize) -> [u8; 8] {
(v as u64).to_le_bytes()
}
#[cfg(test)]
mod tests {
use crate::{
CommutativeHashDigest, CommutativeHashDigestError, ParallelHasher, ParallelHasherError,
Result, SequentialHasher, to_padded_bytes,
};
use itertools::Itertools;
use rand::prelude::*;
use rayon::prelude::*;
use rstest::rstest;
use rstest_reuse::{apply, template};
use sha2::Sha512;
use std::assert_matches;
use std::marker::PhantomData;
use std::num::NonZeroUsize;
trait TestHasher {
fn new(block_size: NonZeroUsize) -> Self
where
Self: Sized;
fn update(&mut self, start_byte: usize, data: &[u8]) -> Result<()>;
fn finalize(self) -> CommutativeHashDigest;
}
impl<Hash> TestHasher for ParallelHasher<Hash>
where
Hash: digest::Digest<OutputSize = digest::consts::U64> + Default,
{
fn new(block_size: NonZeroUsize) -> Self {
ParallelHasher::<Hash>::new(block_size)
}
fn update(&mut self, start_byte: usize, data: &[u8]) -> Result<()> {
ParallelHasher::<Hash>::update(self, start_byte, data)
}
fn finalize(self) -> CommutativeHashDigest {
ParallelHasher::<Hash>::finalize(self)
}
}
impl<Hash> TestHasher for SequentialHasher<Hash>
where
Hash: digest::Digest<OutputSize = digest::consts::U64> + Default,
{
fn new(block_size: NonZeroUsize) -> Self {
SequentialHasher::<Hash>::new(block_size)
}
fn update(&mut self, _start_byte: usize, data: &[u8]) -> Result<()> {
SequentialHasher::<Hash>::update(self, data);
Ok(())
}
fn finalize(self) -> CommutativeHashDigest {
SequentialHasher::<Hash>::finalize(self)
}
}
#[test]
fn test_digest_from_bytes() {
let digest = SequentialHasher::<Sha512>::digest_from_bytes(
NonZeroUsize::new(4).expect("not zero"),
"a short string".as_bytes(),
);
let expected_digest = "ac533343d9d346805bfea710f4ff5d46024d25e46b1286ab29c95b7de12ad877";
assert_eq!(digest.hex_digest(), expected_digest);
}
#[template]
#[rstest]
#[case::parallel(PhantomData::<ParallelHasher::<Sha512>>)]
#[case::sequential(PhantomData::<SequentialHasher::<Sha512>>)]
fn hashers<T: TestHasher>(#[case] _t: PhantomData<T>) {}
#[apply(hashers)]
fn test_one_block<T: TestHasher>(#[case] _t: PhantomData<T>) {
let mut hasher = T::new(NonZeroUsize::new(16).expect("not zero"));
assert_matches!(hasher.update(0, "a short string".as_bytes()), Ok(()));
let expected_digest = "d637c375199003362b880b43c818c8e8ece54ef9e3725d5a7d6e95462f4d1915";
assert_eq!(hasher.finalize().hex_digest(), expected_digest);
}
#[apply(hashers)]
fn test_multiple_blocks<T: TestHasher>(#[case] _t: PhantomData<T>) {
let mut hasher = T::new(NonZeroUsize::new(6).expect("not zero"));
assert_matches!(hasher.update(0, "a short string".as_bytes()), Ok(()));
let expected_digest = "10b93d1d1f852fad1927388fd786135ef3f0e1a8a9e14c6fc1c7c7097fc97c5a";
assert_eq!(hasher.finalize().hex_digest(), expected_digest);
}
#[apply(hashers)]
fn test_multiple_updates<T: TestHasher>(#[case] _t: PhantomData<T>) {
let mut hasher = T::new(NonZeroUsize::new(4).expect("not zero"));
assert_matches!(hasher.update(0, "this".as_bytes()), Ok(()));
assert_matches!(hasher.update(4, "that".as_bytes()), Ok(()));
assert_matches!(hasher.update(8, "more".as_bytes()), Ok(()));
assert_matches!(hasher.update(12, "very".as_bytes()), Ok(()));
assert_matches!(hasher.update(16, "long".as_bytes()), Ok(()));
let expected_digest = "9258f9de43401cd6e8f55545754b84ac58257ec7779723c9790a986daab18206";
assert_eq!(hasher.finalize().hex_digest(), expected_digest);
}
#[apply(hashers)]
fn test_providing_data_chunks_different_hashes_same<T: TestHasher>(#[case] _t: PhantomData<T>) {
let mut hasher1 = T::new(NonZeroUsize::new(4).expect("not zero"));
assert_matches!(hasher1.update(0, "this".as_bytes()), Ok(()));
assert_matches!(hasher1.update(4, "that".as_bytes()), Ok(()));
assert_matches!(hasher1.update(8, "more".as_bytes()), Ok(()));
assert_matches!(hasher1.update(12, "very".as_bytes()), Ok(()));
assert_matches!(hasher1.update(16, "long".as_bytes()), Ok(()));
let digest1 = hasher1.finalize();
let expected_digest = "9258f9de43401cd6e8f55545754b84ac58257ec7779723c9790a986daab18206";
assert_eq!(digest1.hex_digest(), expected_digest);
let mut hasher2 = T::new(NonZeroUsize::new(4).expect("not zero"));
assert_matches!(hasher2.update(0, "thisthat".as_bytes()), Ok(()));
assert_matches!(hasher2.update(8, "moreverylong".as_bytes()), Ok(()));
let digest2 = hasher2.finalize();
assert_eq!(digest2.hex_digest(), expected_digest);
assert_eq!(digest1.to_bytes(), digest2.to_bytes());
}
#[apply(hashers)]
fn test_no_blocks<T: TestHasher>(#[case] _t: PhantomData<T>) {
let hasher = T::new(NonZeroUsize::new(16).expect("not zero"));
let expected_digest = "0000000000000000000000000000000000000000000000000000000000000000";
assert_eq!(hasher.finalize().hex_digest(), expected_digest);
}
#[apply(hashers)]
fn test_different_block_sizes_hash_differently<T: TestHasher>(#[case] _t: PhantomData<T>) {
let mut hasher1 = T::new(NonZeroUsize::new(16).expect("not zero"));
assert_matches!(hasher1.update(0, "a short string".as_bytes()), Ok(()));
let mut hasher2 = T::new(NonZeroUsize::new(4).expect("not zero"));
assert_matches!(hasher2.update(0, "a short string".as_bytes()), Ok(()));
let digest1 = hasher1.finalize();
let digest2 = hasher2.finalize();
assert_ne!(digest1.to_bytes(), digest2.to_bytes(),);
assert_ne!(digest1.hex_digest(), digest2.hex_digest(),);
}
#[apply(hashers)]
fn test_digest<T: TestHasher>(#[case] _t: PhantomData<T>) {
let mut hasher = T::new(NonZeroUsize::new(16).expect("not zero"));
assert_matches!(hasher.update(0, "a short string".as_bytes()), Ok(()));
let digest = hasher.finalize();
let hex_digest = digest.hex_digest();
let new_digest =
CommutativeHashDigest::decode_hex_digest(hex_digest.as_str()).expect("hex to decode");
assert_eq!(new_digest.to_bytes(), digest.to_bytes());
}
#[test]
fn test_misaligned_data() {
let hasher = ParallelHasher::<Sha512>::new(NonZeroUsize::new(16).expect("not zero"));
assert_matches!(
hasher.update(2, "a short string".as_bytes()),
Err(ParallelHasherError::MisalignedData)
);
}
#[test]
fn test_hasher_is_commutative() {
let chunks = ["this", "that", "and more"];
let permutations = [
[0, 1, 2],
[0, 2, 1],
[1, 0, 2],
[1, 2, 0],
[2, 0, 1],
[2, 1, 0],
];
let expected_digest = "9874cae284d8b2e71dea634e932be1049b5e0740964d1bb5db32a03ca81bec28";
for permutation in permutations {
let hasher = ParallelHasher::<Sha512>::new(NonZeroUsize::new(4).expect("not zero"));
for i in permutation {
assert_matches!(hasher.update(i * 4, chunks[i].as_bytes()), Ok(()));
}
assert_eq!(hasher.finalize().hex_digest(), expected_digest);
}
}
fn generate_data(size: usize) -> Vec<u8> {
let seed_value: u64 = 42;
let mut rng = StdRng::seed_from_u64(seed_value);
let mut bytes = vec![0u8; size];
rng.fill_bytes(&mut bytes);
bytes
}
#[test]
fn test_hash_large_value_parallel() {
let expected_digest = "ee7338897f6eaf9b4f26b6ec33d3192bca8231485ac70d64fc81128397af0755";
let data = generate_data(1 << 27); let data_size = data.len();
let chunk_size = 1 << 24; let block_size = 1 << 20; let hasher =
ParallelHasher::<Sha512>::new(NonZeroUsize::new(block_size).expect("not zero"));
(0..data_size)
.step_by(1 << 24)
.collect_vec()
.into_par_iter()
.try_for_each(|start| -> Result<()> {
let buf = &data[start..(start + chunk_size.min(data_size - start))];
hasher.update(start, buf)?;
Ok(())
})
.expect("hashing to succeed");
assert_eq!(hasher.finalize().hex_digest(), expected_digest);
}
#[rstest]
fn test_hash_large_value_sequential(#[values(1, 2, 8, 10, 60, 200)] num_chunks: usize) {
let expected_digest = "ee7338897f6eaf9b4f26b6ec33d3192bca8231485ac70d64fc81128397af0755";
let data = generate_data(1 << 27); let data_size = data.len();
let chunk_size = data_size / num_chunks;
let block_size = 1 << 20; let mut hasher =
SequentialHasher::<Sha512>::new(NonZeroUsize::new(block_size).expect("not zero"));
data.chunks(chunk_size).for_each(|chunk| {
hasher.update(chunk);
});
assert_eq!(hasher.finalize().hex_digest(), expected_digest);
}
#[test]
fn test_decode_hex_digest() {
let digest = "fecf448671b40c18276fc45c7c600be62dd514c87bb614591d151f4db668272b";
assert_eq!(
CommutativeHashDigest::decode_hex_digest(digest)
.expect("to decode")
.hex_digest(),
digest
);
assert_matches!(
CommutativeHashDigest::decode_hex_digest(""),
Err(CommutativeHashDigestError::Not32Bytes)
);
assert_matches!(
CommutativeHashDigest::decode_hex_digest("abc"),
Err(CommutativeHashDigestError::OddLength)
);
assert_matches!(
CommutativeHashDigest::decode_hex_digest("ZYXW"),
Err(CommutativeHashDigestError::InvalidHexCharacter { c: _, index: _ })
);
}
#[test]
fn test_decode_invalid_length_hex_digest() {
assert_matches!(
CommutativeHashDigest::decode_hex_digest("abcd"),
Err(CommutativeHashDigestError::Not32Bytes)
);
}
#[cfg(feature = "serde")]
#[test]
fn test_serialization() {
let digest = SequentialHasher::<Sha512>::digest_from_bytes(
NonZeroUsize::new(16).expect("not zero"),
"a short string".as_bytes(),
);
let serialized = serde_json::to_string(&digest).unwrap();
let deserialized: CommutativeHashDigest = serde_json::from_str(&serialized).unwrap();
assert_eq!(digest, deserialized);
}
#[test]
fn test_to_padded_bytes() {
assert_eq!(to_padded_bytes(16), [16, 0, 0, 0, 0, 0, 0, 0]);
assert_eq!(to_padded_bytes(2_usize.pow(31)), [0, 0, 0, 128, 0, 0, 0, 0]);
}
}