miden-crypto 0.34.0

Miden Cryptographic primitives
Documentation
use alloc::vec::Vec;

use super::{
    Custom, DomainTag, DomainVersion, Eidos, EidosDomain, FeltSequence,
    domain::namespace,
    domains::{GENERIC_FELT_SEQUENCE, RANDOM_COIN_STATE},
    encoding::ODD_LANE_MASK,
    primitive::IV,
};
use crate::{Felt, Word};

#[derive(Debug, Copy, Clone)]
struct TestFeltDomain;

impl EidosDomain for TestFeltDomain {
    type Encoding = FeltSequence;

    const NAME: &'static str = "TEST_FELT";
    const TAG: DomainTag =
        DomainTag::new(namespace::MIDEN_ECOSYSTEM, 0xfffe, DomainVersion::numbered(1));
}

#[derive(Debug, Copy, Clone)]
struct TestParameterizedDomain;

impl EidosDomain for TestParameterizedDomain {
    type Encoding = Custom;

    const NAME: &'static str = "TEST_PARAMETERIZED";
    const TAG: DomainTag =
        DomainTag::new(namespace::MIDEN_ECOSYSTEM, 0xffff, DomainVersion::numbered(0xff));
}

fn felts_seq(n: u32) -> Vec<Felt> {
    (0..n).map(|i| Felt::new_unchecked(i as u64 + 1)).collect()
}

fn word(values: [u64; 4]) -> Word {
    Word::new([
        Felt::new_unchecked(values[0]),
        Felt::new_unchecked(values[1]),
        Felt::new_unchecked(values[2]),
        Felt::new_unchecked(values[3]),
    ])
}

fn assert_digest(actual: Word, expected: [u64; 4]) {
    assert_eq!(actual, word(expected));
}

#[test]
fn frozen_eidos_vectors() {
    assert_digest(
        Eidos::hash_elements::<Felt>(&[]),
        [0x11a6458b66a84073, 0x4ffff7a2ad92252b, 0x3c84006ce9b051f5, 0x55b4b6847dcdfd93],
    );
    assert_digest(
        Eidos::hash(&[]),
        [0x2864baae4689a79a, 0x3b41d3365c3c8860, 0x07fea97b08976abc, 0x3ac995324799df08],
    );
    assert_digest(
        Eidos::hash_elements(&felts_seq(3)),
        [0x17cf960fb0322da4, 0x307f9ec74ec5b4d7, 0x798ca1783855c9ec, 0x61909a51c2d68383],
    );
    assert_digest(
        Eidos::hash(b"abc"),
        [0x365b33e5d73475c7, 0x7842d2a7da672026, 0x080ccab96956c05a, 0x1c431bb11d6f35ee],
    );
    assert_digest(
        Eidos::hash_elements_in_domain(&felts_seq(4), RANDOM_COIN_STATE),
        [0x275e1958ad7c08dc, 0x12b8f7731460be11, 0x29718531efa9484b, 0x4e32493e94faf462],
    );
    assert_digest(
        Eidos::hash_elements(&felts_seq(9)),
        [0x4d59f4c52d792900, 0x79244370f96f6467, 0x748d893146975bd4, 0x3fda88c89a46ce40],
    );
    let bytes: Vec<u8> = (0..65).map(|i| i as u8).collect();
    assert_digest(
        Eidos::hash(&bytes),
        [0x0a3700b1a8d0e65c, 0x389e810e5f3cdab2, 0x36cd639c19ced5e9, 0x4e8a2f58fb01c8ac],
    );
}

#[test]
fn felt_and_byte_constructions_are_separated_at_boundary_inputs() {
    assert_ne!(Eidos::hash(&[]), Eidos::hash_elements::<Felt>(&[]));
    assert_ne!(Eidos::hash(&[1]), Eidos::hash_elements(&[Felt::ONE]));
    assert_ne!(Eidos::hash(&[0u8; 64]), Eidos::hash_elements(&[Felt::ZERO; 8]));
}

#[test]
fn different_lengths_within_same_block_diverge() {
    let one = vec![Felt::new_unchecked(7)];
    let two = vec![Felt::new_unchecked(7), Felt::ZERO];

    assert_ne!(Eidos::hash_elements(&one), Eidos::hash_elements(&two));
}

#[test]
fn empty_input_is_not_zero_word() {
    assert_ne!(Eidos::hash_elements::<Felt>(&[]), Word::default());
    assert_ne!(Eidos::hash(&[]), Word::default());
}

#[test]
fn different_domains_diverge() {
    let xs = felts_seq(4);
    let d0 = Eidos::hash_elements_in_domain(&xs, GENERIC_FELT_SEQUENCE);
    let d1 = Eidos::hash_elements_in_domain(&xs, RANDOM_COIN_STATE);
    let d2 = Eidos::hash_elements_in_domain(&xs, TestFeltDomain);

    assert_ne!(d0, d1);
    assert_ne!(d0, d2);
    assert_ne!(d1, d2);
}

#[test]
fn hash_elements_equals_the_generic_felt_domain() {
    let xs = felts_seq(8);

    assert_eq!(
        Eidos::hash_elements(&xs),
        Eidos::hash_elements_in_domain(&xs, GENERIC_FELT_SEQUENCE)
    );
}

#[test]
fn generic_initializer_has_the_exact_lane_layout() {
    let params = [u32::MAX; 3];
    let cv = Eidos::init_chaining_word_with_params(TestParameterizedDomain, params);
    let lanes: [u32; 8] = cv
        .as_elements()
        .iter()
        .flat_map(|felt| {
            let packed = felt.as_canonical_u64();
            [packed as u32, (packed >> 32) as u32]
        })
        .collect::<Vec<_>>()
        .try_into()
        .unwrap();

    assert_eq!(
        lanes,
        [
            TestParameterizedDomain::TAG.as_u32(),
            IV[1] & ODD_LANE_MASK,
            params[0],
            IV[3] & ODD_LANE_MASK,
            params[1],
            IV[5] & ODD_LANE_MASK,
            params[2],
            IV[7] & ODD_LANE_MASK,
        ]
    );
    assert_eq!(
        Eidos::init_chaining_word(TestParameterizedDomain, params[0]),
        Eidos::init_chaining_word_with_params(TestParameterizedDomain, [params[0], 0, 0]),
    );
}

#[test]
fn merge_uses_the_reserved_zero_tuple_not_the_generic_felt_domain() {
    let left = word([1, 2, 3, 4]);
    let right = word([5, 6, 7, 8]);
    let block = [left[0], left[1], left[2], left[3], right[0], right[1], right[2], right[3]];

    assert_eq!(
        Eidos::merge(&[left, right]),
        Eidos::compress(Eidos::merkle_node_init_chaining_word(), block)
    );
    assert_ne!(Eidos::merge(&[left, right]), Eidos::hash_elements(&block));
}

#[test]
fn merkle_initializer_has_zero_in_every_injected_lane() {
    let lanes: [u32; 8] = Eidos::merkle_node_init_chaining_word()
        .as_elements()
        .iter()
        .flat_map(|felt| {
            let packed = felt.as_canonical_u64();
            [packed as u32, (packed >> 32) as u32]
        })
        .collect::<Vec<_>>()
        .try_into()
        .unwrap();

    assert_eq!(
        lanes,
        [
            0,
            IV[1] & ODD_LANE_MASK,
            0,
            IV[3] & ODD_LANE_MASK,
            0,
            IV[5] & ODD_LANE_MASK,
            0,
            IV[7] & ODD_LANE_MASK,
        ]
    );
}

#[test]
fn merge_in_domain_matches_hash_elements_in_domain() {
    let left = word([10, 20, 30, 40]);
    let right = word([50, 60, 70, 80]);
    let concat = vec![left[0], left[1], left[2], left[3], right[0], right[1], right[2], right[3]];

    assert_eq!(
        Eidos::merge_in_domain(&[left, right], TestFeltDomain),
        Eidos::hash_elements_in_domain(&concat, TestFeltDomain)
    );
}

#[test]
fn merge_many_matches_hash_elements_on_concat() {
    let words = vec![word([1, 2, 3, 4]), word([5, 6, 7, 8]), word([9, 10, 11, 12])];
    let mut concat = Vec::new();
    for w in &words {
        concat.extend_from_slice(w.as_ref());
    }

    assert_eq!(Eidos::merge_many(&words), Eidos::hash_elements(&concat));
}

#[test]
fn felt_construction_block_boundary_lengths() {
    let lengths = [1u32, 4, 8, 9, 17];
    let digests: Vec<Word> = lengths.iter().map(|&n| Eidos::hash_elements(&felts_seq(n))).collect();

    for i in 0..digests.len() {
        for j in (i + 1)..digests.len() {
            assert_ne!(
                digests[i], digests[j],
                "lengths {} and {} collided",
                lengths[i], lengths[j]
            );
        }
    }
}

#[test]
fn byte_construction_block_boundary_lengths() {
    let lengths = [0usize, 1, 63, 64, 65, 128];
    let digests: Vec<Word> = lengths
        .iter()
        .map(|&n| {
            let bytes: Vec<u8> = (0..n).map(|i| (i & 0xff) as u8).collect();
            Eidos::hash(&bytes)
        })
        .collect();

    for i in 0..digests.len() {
        for j in (i + 1)..digests.len() {
            assert_ne!(
                digests[i], digests[j],
                "byte lengths {} and {} collided",
                lengths[i], lengths[j]
            );
        }
    }
}

#[test]
fn frozen_merge_and_challenger_vectors() {
    use p3_challenger::{CanObserve, CanSample};

    use super::MidenEidosChallenger;

    let merged = Eidos::merge(&[word([1, 2, 3, 4]), word([5, 6, 7, 8])]);
    assert_digest(
        merged,
        [0x4d75748c8d801fcb, 0x08777791a35ff853, 0x1b245de8521c0075, 0x24e16f44209db86c],
    );

    let mut challenger = MidenEidosChallenger::new(word([1, 2, 3, 4]), word([10, 11, 12, 13]));
    for value in 20..=24 {
        challenger.observe(Felt::new_unchecked(value));
    }
    let first = Word::new(core::array::from_fn(|_| CanSample::<Felt>::sample(&mut challenger)));
    let second = Word::new(core::array::from_fn(|_| CanSample::<Felt>::sample(&mut challenger)));
    assert_digest(
        first,
        [
            9064457378334718372,
            5425353699759013086,
            1604522722744930894,
            6404602263707938109,
        ],
    );
    assert_digest(
        second,
        [
            4259844014858609293,
            8079007960973284947,
            8487760873676030018,
            4187353069166526105,
        ],
    );
}