soma-som-core 0.1.0

Universal soma(som) structural primitives — Quad / Tree / Ring / Genesis / Fingerprint / TemporalLedger / CrossingRecord
Documentation
// SPDX-License-Identifier: LGPL-3.0-only
#![allow(missing_docs)]

//! Fingerprint computation: awareness (72 positions) and system (90 positions).
//!
//! ## Spec traceability
//! - Definition 7 (Spec §8.4): Awareness fingerprint — 72 SOM positions
//! - Definition 8 (Spec §8.5): System fingerprint — 90 positions (72 SOM + 18 SOMA)
//! - Criterion 2 (Spec §11): Fingerprint sensitivity — altering any single
//!   Quad value at any single position must produce a different fingerprint
//! - All hash operations use BLAKE3

use crate::quad::Quad;
use crate::ring::Ring;

/// Compute the awareness fingerprint from a slice of SOM Quads.
///
/// ## Spec Definition 7
///
/// > The awareness fingerprint is the cryptographic digest of the complete
/// > SOM state across all units and layers: |F_awareness| = 72
///
/// The input must be exactly 24 Quads (6 units × 4 layers) in canonical order:
/// FU.Data, FU.Server, FU.Client, FU.Interface, MU.Data, ... HU.Interface.
///
/// Each Quad contributes its root, pointer, and tree to the hash in order.
/// The tree entries are hashed in deterministic key order (BTreeMap guarantees this).
///
/// ## Criterion 2 (Fingerprint sensitivity)
///
/// Because each position's content_hash is fed sequentially into a BLAKE3 hasher,
/// any single-byte change at any position propagates through the hash. This
/// satisfies the requirement that "altering any single Quad value at any single
/// SOM layer in any single unit must produce a different awareness fingerprint."
pub fn awareness_fingerprint(som_quads: &[&Quad]) -> [u8; 32] {
    assert_eq!(
        som_quads.len(),
        24,
        "awareness fingerprint requires exactly 24 SOM Quads (6 units × 4 layers)"
    );

    let mut hasher = blake3::Hasher::new();

    for quad in som_quads {
        // Feed each element individually for maximum sensitivity.
        // A change to root, pointer, or any tree entry changes the digest.
        hasher.update(&quad.root);
        hasher.update(&quad.pointer);

        // Tree entries in deterministic order
        for (key, value) in &quad.tree {
            hasher.update(key.as_bytes());
            hasher.update(&(value.len() as u64).to_le_bytes());
            hasher.update(value);
        }
    }

    *hasher.finalize().as_bytes()
}

/// Compute the system fingerprint from all 30 Quads (24 SOM + 6 SOMA).
///
/// ## Spec Definition 8
///
/// > The system fingerprint includes the 6 SOMA Quads (18 positions) in
/// > addition to the 72 SOM positions: |F_system| = 90
///
/// The system fingerprint captures the awareness state *and* the functional
/// configuration that produced it.
pub fn system_fingerprint(som_quads: &[&Quad], soma_quads: &[&Quad]) -> [u8; 32] {
    assert_eq!(som_quads.len(), 24);
    assert_eq!(soma_quads.len(), 6);

    let mut hasher = blake3::Hasher::new();

    // First: all 72 SOM positions
    for quad in som_quads {
        hasher.update(&quad.root);
        hasher.update(&quad.pointer);
        for (key, value) in &quad.tree {
            hasher.update(key.as_bytes());
            hasher.update(&(value.len() as u64).to_le_bytes());
            hasher.update(value);
        }
    }

    // Then: all 18 SOMA positions
    for quad in soma_quads {
        hasher.update(&quad.root);
        hasher.update(&quad.pointer);
        for (key, value) in &quad.tree {
            hasher.update(key.as_bytes());
            hasher.update(&(value.len() as u64).to_le_bytes());
            hasher.update(value);
        }
    }

    *hasher.finalize().as_bytes()
}

/// Convenience: compute awareness fingerprint directly from a Ring.
pub fn ring_awareness_fingerprint(ring: &Ring) -> [u8; 32] {
    let quads = ring.all_som_quads();
    awareness_fingerprint(&quads)
}

/// Convenience: compute system fingerprint directly from a Ring.
pub fn ring_system_fingerprint(ring: &Ring) -> [u8; 32] {
    let som = ring.all_som_quads();
    let soma = ring.all_soma_quads();
    system_fingerprint(&som, &soma)
}

/// Derive a ring's unique identity from its system fingerprint and boundary key.
///
/// ## Ring Identity for Federation
///
/// The `RingFingerprint` uniquely identifies a ring instance in the Meta-Ring
/// federation. It combines two ring-internal values:
/// - `system_fingerprint`: captures ring state (90 Quad positions)
/// - `boundary_key`: the boundary's Ed25519 verifying key (32 bytes)
///
/// Domain separation (`b"ring:"` prefix) ensures this hash cannot collide
/// with any other BLAKE3 usage in the system.
pub fn ring_fingerprint(system_fingerprint: &[u8; 32], boundary_key: &[u8; 32]) -> [u8; 32] {
    let mut hasher = blake3::Hasher::new();
    hasher.update(b"ring:");
    hasher.update(system_fingerprint);
    hasher.update(boundary_key);
    *hasher.finalize().as_bytes()
}

/// Compute a single-position hash for a specific (unit, layer) Quad.
///
/// Useful for testing fingerprint sensitivity: hash one position, change it,
/// verify the overall fingerprint changes.
pub fn position_hash(quad: &Quad) -> [u8; 32] {
    quad.content_hash()
}

// inline: exercises module-private items via super::*
#[cfg(test)]
mod tests {
    use super::*;
    use crate::quad::Tree;
    use crate::ring::Ring;
    use crate::types::{Layer, UnitId};

    fn make_populated_ring() -> Ring {
        let mut ring = Ring::new();
        for &unit in &UnitId::ALL {
            let us = ring.unit_mut(unit);
            us.soma_quad = Quad::from_strings(
                &format!("{unit}.soma.root"),
                &format!("{unit}.soma.ptr"),
                Tree::new(),
            );
            for &layer in &Layer::ALL {
                let mut tree = Tree::new();
                tree.insert(
                    format!("{unit}.{layer}.data"),
                    format!("value-{unit}-{layer}").into_bytes(),
                );
                *us.som_quad_mut(layer) = Quad::from_strings(
                    &format!("{unit}.{layer}.root"),
                    &format!("{unit}.{layer}.ptr"),
                    tree,
                );
            }
        }
        ring
    }

    #[test]
    fn awareness_fingerprint_is_deterministic() {
        let ring = make_populated_ring();
        let fp1 = ring_awareness_fingerprint(&ring);
        let fp2 = ring_awareness_fingerprint(&ring);
        assert_eq!(fp1, fp2);
    }

    #[test]
    fn system_fingerprint_is_deterministic() {
        let ring = make_populated_ring();
        let fp1 = ring_system_fingerprint(&ring);
        let fp2 = ring_system_fingerprint(&ring);
        assert_eq!(fp1, fp2);
    }

    #[test]
    fn system_fingerprint_differs_from_awareness() {
        let ring = make_populated_ring();
        let a = ring_awareness_fingerprint(&ring);
        let s = ring_system_fingerprint(&ring);
        // System includes SOMA Quads, so it must differ
        assert_ne!(a, s);
    }

    #[test]
    fn criterion_2_single_position_change_changes_awareness_fingerprint() {
        // "Altering any single Quad value at any single SOM layer in any
        //  single unit must produce a different awareness fingerprint."
        let ring = make_populated_ring();
        let original_fp = ring_awareness_fingerprint(&ring);

        // Test: change ONE value in ONE layer of ONE unit
        for &unit in &UnitId::ALL {
            for &layer in &Layer::ALL {
                let mut modified_ring = ring.clone();
                let quad = modified_ring.unit_mut(unit).som_quad_mut(layer);

                // Modify the root
                quad.root[0] ^= 0xFF;
                let new_fp = ring_awareness_fingerprint(&modified_ring);
                assert_ne!(
                    original_fp, new_fp,
                    "Changing root at {unit}.{layer} must change the fingerprint"
                );

                // Restore and modify the pointer
                let mut modified_ring = ring.clone();
                let quad = modified_ring.unit_mut(unit).som_quad_mut(layer);
                quad.pointer[0] ^= 0xFF;
                let new_fp = ring_awareness_fingerprint(&modified_ring);
                assert_ne!(
                    original_fp, new_fp,
                    "Changing pointer at {unit}.{layer} must change the fingerprint"
                );

                // Restore and modify the tree
                let mut modified_ring = ring.clone();
                let quad = modified_ring.unit_mut(unit).som_quad_mut(layer);
                quad.tree.insert("mutated".into(), vec![0xFF]);
                let new_fp = ring_awareness_fingerprint(&modified_ring);
                assert_ne!(
                    original_fp, new_fp,
                    "Changing tree at {unit}.{layer} must change the fingerprint"
                );
            }
        }
    }

    #[test]
    fn criterion_2_all_72_positions_produce_distinct_fingerprints() {
        let ring = make_populated_ring();

        let mut fingerprints = Vec::with_capacity(72);

        // For each of the 72 SOM positions, mutate just that position
        for &unit in &UnitId::ALL {
            for &layer in &Layer::ALL {
                // Mutate root
                let mut mr = ring.clone();
                mr.unit_mut(unit).som_quad_mut(layer).root[0] ^= 0xFF;
                fingerprints.push(ring_awareness_fingerprint(&mr));

                // Mutate pointer
                let mut mr = ring.clone();
                mr.unit_mut(unit).som_quad_mut(layer).pointer[0] ^= 0xFF;
                fingerprints.push(ring_awareness_fingerprint(&mr));

                // Mutate tree
                let mut mr = ring.clone();
                mr.unit_mut(unit)
                    .som_quad_mut(layer)
                    .tree
                    .insert("_criterion2_probe".into(), vec![unit as u8, layer as u8]);
                fingerprints.push(ring_awareness_fingerprint(&mr));
            }
        }

        // All fingerprints must be distinct (no collisions across single-position changes)
        let count = fingerprints.len();
        fingerprints.sort();
        fingerprints.dedup();
        assert_eq!(
            fingerprints.len(),
            count,
            "All single-position mutations must produce distinct fingerprints"
        );
    }

    // ── ring_fingerprint BDD tests ───────────���────────────────

    // S1 — ring_fingerprint is deterministic
    #[test]
    fn ring_fingerprint_deterministic() {
        let sys = [0xAAu8; 32];
        let bkey = [0xBBu8; 32];
        let fp1 = ring_fingerprint(&sys, &bkey);
        let fp2 = ring_fingerprint(&sys, &bkey);
        assert_eq!(fp1, fp2);
    }

    // S2 — different system_fingerprint produces different ring_fingerprint
    #[test]
    fn ring_fingerprint_differs_on_system() {
        let bkey = [0xBBu8; 32];
        let fp1 = ring_fingerprint(&[0x01u8; 32], &bkey);
        let fp2 = ring_fingerprint(&[0x02u8; 32], &bkey);
        assert_ne!(fp1, fp2);
    }

    // S3 — different boundary_key produces different ring_fingerprint
    #[test]
    fn ring_fingerprint_differs_on_boundary_key() {
        let sys = [0xAAu8; 32];
        let fp1 = ring_fingerprint(&sys, &[0x01u8; 32]);
        let fp2 = ring_fingerprint(&sys, &[0x02u8; 32]);
        assert_ne!(fp1, fp2);
    }

    // S4 — domain separation: ring_fingerprint differs from raw hash
    #[test]
    fn ring_fingerprint_domain_separated() {
        let sys = [0xAAu8; 32];
        let bkey = [0xBBu8; 32];
        let fp = ring_fingerprint(&sys, &bkey);

        // Hash the same data without the domain prefix
        let mut raw_hasher = blake3::Hasher::new();
        raw_hasher.update(&sys);
        raw_hasher.update(&bkey);
        let raw = *raw_hasher.finalize().as_bytes();

        assert_ne!(fp, raw, "domain prefix must differentiate the hash");
    }
}