icydb-core 0.277.1

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
//! Module: value::hash::tests
//! Covers the frozen seed, version, and digest vectors for canonical value
//! hashing.

use super::*;
use crate::{
    types::{Decimal, Float32 as F32, Float64 as F64, Principal, Subaccount},
    value::{Value, ValueEnum},
};

#[test]
fn hash_contract_seed_and_version_are_frozen() {
    assert_eq!(VALUE_HASH_SEED, 0);
    assert_eq!(VALUE_HASH_VERSION, 1);
}

#[test]
fn hash_digest_contract_vectors_are_frozen_for_upgrade_stability() {
    let vectors = vec![
        (
            "null",
            Value::Null,
            0xdfaf_9423_b3de_9944_0ba8_26ad_5663_8a12,
        ),
        (
            "empty_list",
            Value::List(Vec::new()),
            0xf8c9_06ce_8fc3_ad0b_d71d_c7b2_be94_b6cd,
        ),
        (
            "empty_map",
            Value::Map(Vec::new()),
            0xc961_6022_4099_6188_601a_2e1d_a3d4_577d,
        ),
        (
            "nat_42",
            Value::Nat64(42),
            0xa401_17bf_4119_7e82_bb21_ed31_5d8c_f992,
        ),
        (
            "int_neg7",
            Value::Int64(-7),
            0x7470_6cc5_9093_df80_0d3b_e517_da6b_0104,
        ),
        (
            "text_alpha",
            Value::Text("alpha".to_string()),
            0x0123_51fb_30ed_4d9f_0946_7c2c_7595_6fb0,
        ),
        (
            "decimal_1",
            Value::Decimal(Decimal::new(10, 1)),
            0x7d42_1e3f_fffc_9100_0be6_fa20_26b6_0b82,
        ),
        (
            "map_a1_z9",
            Value::Map(vec![
                (Value::Text("a".to_string()), Value::Nat64(1)),
                (Value::Text("z".to_string()), Value::Nat64(9)),
            ]),
            0xbd7e_f83f_fb8d_d205_445f_9762_9732_4182,
        ),
        (
            "nested_list_map",
            Value::List(vec![
                Value::Map(vec![
                    (Value::Text("z".to_string()), Value::Nat64(9)),
                    (Value::Text("a".to_string()), Value::Nat64(1)),
                ]),
                Value::Decimal(Decimal::new(25, 0)),
            ]),
            0xfff3_270d_358d_8057_8e24_f082_192e_a010,
        ),
    ];
    for (label, value, expected_digest) in vectors {
        let actual_digest = hash_value(&value).expect("hash value");
        assert_eq!(
            u128::from_be_bytes(actual_digest),
            expected_digest,
            "value-hash digest vector drift for {label}; canonical encoding/seed/version contract changed",
        );
    }
}

fn v_f64(x: f64) -> Value {
    Value::Float64(F64::try_new(x).expect("finite f64"))
}
fn v_f32(x: f32) -> Value {
    Value::Float32(F32::try_new(x).expect("finite f32"))
}
fn v_i(x: i64) -> Value {
    Value::Int64(x)
}
fn v_txt(s: &str) -> Value {
    Value::Text(s.to_string())
}

#[test]
fn hash_is_deterministic_for_int() {
    let v = Value::Int64(42);
    let a = hash_value(&v).expect("hash value");
    let b = hash_value(&v).expect("hash value");
    assert_eq!(a, b, "hash should be deterministic for same value");
}

#[test]
fn different_variants_produce_different_hashes() {
    let a = hash_value(&Value::Int64(5)).expect("hash value");
    let b = hash_value(&Value::Nat64(5)).expect("hash value");
    assert_ne!(
        a, b,
        "Int(5) and Nat(5) must hash differently (different tag)"
    );
}

#[test]
fn enum_hash_tracks_type_identity() {
    let first_type = Value::Enum(ValueEnum::test_unit(1, 1));
    let second_type = Value::Enum(ValueEnum::test_unit(2, 1));
    assert_ne!(
        hash_value(&first_type).expect("hash value"),
        hash_value(&second_type).expect("hash value"),
        "enum hashes must include store-local type identity"
    );
}

#[test]
fn enum_hash_includes_payload() {
    let with_one = Value::Enum(ValueEnum::test_payload(1, 1, Value::Nat64(1)));
    let with_two = Value::Enum(ValueEnum::test_payload(1, 1, Value::Nat64(2)));

    assert_ne!(
        hash_value(&with_one).expect("hash value"),
        hash_value(&with_two).expect("hash value"),
        "Enum payload must influence hash/fingerprint"
    );
}

#[test]
fn float32_and_float64_hash_differ() {
    let a = hash_value(&v_f32(1.0)).expect("hash value");
    let b = hash_value(&v_f64(1.0)).expect("hash value");
    assert_ne!(
        a, b,
        "Float32 and Float64 must hash differently (different tag)"
    );
}

#[test]
fn decimal_hash_normalizes_equivalent_values() {
    let one = Value::Decimal(Decimal::new(1, 0));
    let one_point_zero = Value::Decimal(Decimal::new(10, 1));

    assert_eq!(
        one, one_point_zero,
        "decimal values should be semantically equal"
    );
    assert_eq!(
        hash_value(&one).expect("hash value"),
        hash_value(&one_point_zero).expect("hash value"),
        "equivalent decimals must hash to the same fingerprint"
    );
}

#[test]
fn text_is_length_and_content_sensitive() {
    let a = hash_value(&v_txt("foo")).expect("hash value");
    let b = hash_value(&v_txt("bar")).expect("hash value");
    assert_ne!(a, b, "different strings should hash differently");

    let c = hash_value(&v_txt("foo")).expect("hash value");
    assert_eq!(a, c, "same string should hash the same");
}

#[test]
fn list_hash_is_order_sensitive() {
    let l1 = Value::from_slice(&[v_i(1), v_i(2)]);
    let l2 = Value::from_slice(&[v_i(2), v_i(1)]);
    assert_ne!(
        hash_value(&l1).expect("hash value"),
        hash_value(&l2).expect("hash value"),
        "list order should affect hash"
    );
}

#[test]
fn list_hash_is_length_sensitive() {
    let l1 = Value::from_slice(&[v_i(1)]);
    let l2 = Value::from_slice(&[v_i(1), v_i(1)]);
    assert_ne!(
        hash_value(&l1).expect("hash value"),
        hash_value(&l2).expect("hash value"),
        "list length should affect hash"
    );
}

#[test]
fn list_blob_boundaries_are_length_framed() {
    let left = Value::List(vec![
        Value::Blob(vec![0x10, 0xFF, 0x02, 0x11]),
        Value::Blob(vec![0x12]),
    ]);
    let right = Value::List(vec![
        Value::Blob(vec![0x10]),
        Value::Blob(vec![0x11, 0xFF, 0x02, 0x12]),
    ]);

    assert_ne!(
        hash_value(&left).expect("hash value"),
        hash_value(&right).expect("hash value"),
        "blob boundaries must be length-framed to avoid collisions"
    );
}

#[test]
fn map_hash_is_order_independent_for_non_canonical_construction_order() {
    let left = Value::Map(vec![
        (Value::Text("z".to_string()), Value::Nat64(9)),
        (Value::Text("a".to_string()), Value::Nat64(1)),
    ]);
    let right = Value::Map(vec![
        (Value::Text("a".to_string()), Value::Nat64(1)),
        (Value::Text("z".to_string()), Value::Nat64(9)),
    ]);

    assert_eq!(
        hash_value(&left).expect("hash value"),
        hash_value(&right).expect("hash value"),
        "map hash must be deterministic regardless of insertion order"
    );
}

#[test]
fn single_list_identity_canonical_hash_matches_generic_hash_contract() {
    let values = vec![
        Value::Account(crate::types::Account::from_owner_and_subaccount(
            Principal::from_slice(&[7]),
            Some(Subaccount::from_array([7; 32])),
        )),
        Value::Blob(vec![1u8, 2u8, 3u8]),
        Value::Bool(true),
        Value::Date(crate::types::Date::try_new(2024, 1, 2).expect("valid date")),
        Value::Duration(crate::types::Duration::from_secs(1)),
        Value::Float32(F32::try_new(1.25).expect("finite f32")),
        Value::Float64(F64::try_new(2.5).expect("finite f64")),
        Value::Int64(-7),
        Value::Int128(123i128),
        Value::Principal(crate::types::Principal::from_slice(&[1u8, 2u8, 3u8])),
        Value::Subaccount(crate::types::Subaccount::from_array([1u8; 32])),
        Value::Text("example".to_string()),
        Value::Timestamp(crate::types::Timestamp::from_secs(1)),
        Value::Nat64(7),
        Value::Nat128(9u128),
        Value::Ulid(crate::types::Ulid::from_u128(42)),
    ];

    for value in values {
        let fast = hash_single_list_identity_canonical_value(&value)
            .expect("fast single-list hash should succeed")
            .expect("value should stay on the identity-canonical fast path");
        let generic = hash_value(&Value::List(vec![value]))
            .expect("generic grouped single-list hash should succeed");

        assert_eq!(
            fast, generic,
            "fast single-list identity-canonical hash must match generic grouped hash contract",
        );
    }
}

#[test]
fn cryptographic_fingerprints_stream_current_canonical_bytes() {
    for (value, bytes) in [
        (
            Value::Text("victim".into()),
            b"\x01\x11\x00\x00\x00\x06victim".to_vec(),
        ),
        (
            Value::Blob(vec![0, 1, 255]),
            vec![1, 2, 1, 0, 0, 0, 3, 0, 1, 255],
        ),
        (Value::Null, vec![1, 14]),
        (Value::Unit, vec![1, 22]),
    ] {
        assert_eq!(
            fingerprint_value(&value).unwrap(),
            <[u8; 32]>::from(Sha256::digest(bytes))
        );
    }
    let entries = vec![
        (Value::Text("b".into()), Value::Nat64(2)),
        (Value::Text("a".into()), Value::Nat64(1)),
    ];
    assert_eq!(
        fingerprint_value(&Value::Map(entries.clone())).unwrap(),
        fingerprint_value(&Value::Map(entries.into_iter().rev().collect())).unwrap()
    );
    assert_eq!(
        fingerprint_value(&Value::Decimal(Decimal::new(100, 2))).unwrap(),
        fingerprint_value(&Value::Decimal(Decimal::new(1, 0))).unwrap()
    );
}