//! HeavyHash matrix operations in GF(2^8). //! //! The matrix is a 64x64 matrix where each element is in GF(2^8). //! Matrix multiplication is performed using Galois field arithmetic. /// The irreducible polynomial for GF(2^8): x^8 + x^4 + x^3 + x + 1 const GF_POLY: u16 = 0x11B; /// 64x64 HeavyHash matrix. /// This is deterministically generated from a seed. #[derive(Clone)] pub struct HeavyMatrix { /// The 64x64 matrix elements. data: [[u8; 64]; 64], } impl HeavyMatrix { /// Creates the default HeavyMatrix (Kaspa-compatible). pub fn new() -> Self { Self::from_seed(&SYNOR_MATRIX_SEED) } /// Creates a matrix from a 32-byte seed. pub fn from_seed(seed: &[u8; 32]) -> Self { let mut matrix = [[0u8; 64]; 64]; let mut state = *seed; for row in matrix.iter_mut() { for elem in row.iter_mut() { // Simple deterministic expansion using Blake3 state = blake3::hash(&state).into(); *elem = state[0]; } } HeavyMatrix { data: matrix } } /// Multiplies a 256-bit vector by the matrix. /// Input: 32 bytes (256 bits) treated as 64 nibbles /// Output: 32 bytes after matrix multiplication #[allow(clippy::needless_range_loop)] pub fn multiply(&self, input: &[u8; 32]) -> [u8; 32] { // Expand 32 bytes into 64 nibbles (4 bits each) let mut nibbles = [0u8; 64]; for i in 0..32 { nibbles[i * 2] = (input[i] >> 4) & 0x0F; nibbles[i * 2 + 1] = input[i] & 0x0F; } // Matrix multiplication in GF(2^8) let mut result = [0u8; 64]; for i in 0..64 { let mut acc = 0u8; for j in 0..64 { acc = gf_add(acc, gf_mul(self.data[i][j], nibbles[j])); } result[i] = acc; } // Compress 64 values back to 32 bytes let mut output = [0u8; 32]; for i in 0..32 { // XOR adjacent pairs output[i] = result[i * 2] ^ result[i * 2 + 1]; } output } /// Multiplies with full byte input (alternative method). #[allow(clippy::needless_range_loop)] pub fn multiply_bytes(&self, input: &[u8; 32]) -> [u8; 32] { let mut output = [0u8; 32]; // Treat input as 32 bytes, output as 32 bytes // Use a 32x32 view of the matrix for i in 0..32 { let mut acc: u16 = 0; for j in 0..32 { let m1 = self.data[i * 2][j * 2] as u16; let m2 = self.data[i * 2 + 1][j * 2 + 1] as u16; let v = input[j] as u16; acc = acc.wrapping_add((m1.wrapping_mul(v)).wrapping_add(m2.wrapping_mul(v))); } output[i] = (acc & 0xFF) as u8; } output } /// Gets a matrix element. #[inline] pub fn get(&self, row: usize, col: usize) -> u8 { self.data[row][col] } /// Returns the raw matrix data. pub fn as_bytes(&self) -> &[[u8; 64]; 64] { &self.data } } impl Default for HeavyMatrix { fn default() -> Self { Self::new() } } /// Addition in GF(2^8) is XOR. #[inline] pub fn gf_add(a: u8, b: u8) -> u8 { a ^ b } /// Multiplication in GF(2^8) using the irreducible polynomial. #[inline] pub fn gf_mul(a: u8, b: u8) -> u8 { let mut result: u16 = 0; let mut aa = a as u16; let mut bb = b as u16; for _ in 0..8 { if bb & 1 != 0 { result ^= aa; } let hi_bit = aa & 0x80; aa <<= 1; if hi_bit != 0 { aa ^= GF_POLY; } bb >>= 1; } (result & 0xFF) as u8 } /// Precomputed multiplication tables for faster GF(2^8) operations. pub struct GfTables { /// Multiplication table: mul[a][b] = a * b in GF(2^8) mul: [[u8; 256]; 256], } impl GfTables { /// Creates precomputed tables. #[allow(clippy::needless_range_loop)] pub fn new() -> Self { let mut mul = [[0u8; 256]; 256]; for a in 0..256 { for b in 0..256 { mul[a][b] = gf_mul(a as u8, b as u8); } } GfTables { mul } } /// Fast multiplication using lookup table. #[inline] pub fn mul(&self, a: u8, b: u8) -> u8 { self.mul[a as usize][b as usize] } } impl Default for GfTables { fn default() -> Self { Self::new() } } /// Synor's matrix seed (unique to our chain). /// This creates a different matrix than Kaspa, preventing merge-mining. pub const SYNOR_MATRIX_SEED: [u8; 32] = [ 0x53, 0x59, 0x4E, 0x4F, 0x52, 0x5F, 0x48, 0x45, // "SYNOR_HE" 0x41, 0x56, 0x59, 0x5F, 0x4D, 0x41, 0x54, 0x52, // "AVY_MATR" 0x49, 0x58, 0x5F, 0x53, 0x45, 0x45, 0x44, 0x5F, // "IX_SEED_" 0x56, 0x30, 0x30, 0x31, 0x00, 0x00, 0x00, 0x01, // "V001"... ]; /// Optimized matrix multiplication using precomputed tables. pub struct OptimizedMatrix { /// The 64x64 matrix. matrix: HeavyMatrix, /// Precomputed GF tables. tables: GfTables, } impl OptimizedMatrix { /// Creates an optimized matrix with lookup tables. pub fn new() -> Self { OptimizedMatrix { matrix: HeavyMatrix::new(), tables: GfTables::new(), } } /// Creates from a specific seed. pub fn from_seed(seed: &[u8; 32]) -> Self { OptimizedMatrix { matrix: HeavyMatrix::from_seed(seed), tables: GfTables::new(), } } /// Fast matrix multiplication. #[allow(clippy::needless_range_loop)] pub fn multiply(&self, input: &[u8; 32]) -> [u8; 32] { // Expand to nibbles let mut nibbles = [0u8; 64]; for i in 0..32 { nibbles[i * 2] = (input[i] >> 4) & 0x0F; nibbles[i * 2 + 1] = input[i] & 0x0F; } // Matrix multiplication with table lookup let mut result = [0u8; 64]; for i in 0..64 { let mut acc = 0u8; for j in 0..64 { acc ^= self.tables.mul(self.matrix.get(i, j), nibbles[j]); } result[i] = acc; } // Compress let mut output = [0u8; 32]; for i in 0..32 { output[i] = result[i * 2] ^ result[i * 2 + 1]; } output } /// Returns the underlying matrix. pub fn matrix(&self) -> &HeavyMatrix { &self.matrix } } impl Default for OptimizedMatrix { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_gf_add() { assert_eq!(gf_add(0, 0), 0); assert_eq!(gf_add(0xFF, 0xFF), 0); assert_eq!(gf_add(0xAA, 0x55), 0xFF); } #[test] fn test_gf_mul() { // Multiplication by 1 assert_eq!(gf_mul(0x53, 1), 0x53); // Multiplication by 0 assert_eq!(gf_mul(0x53, 0), 0); // Known values assert_eq!(gf_mul(0x57, 0x83), 0xC1); } #[test] fn test_matrix_deterministic() { let m1 = HeavyMatrix::new(); let m2 = HeavyMatrix::new(); // Same seed should produce same matrix for i in 0..64 { for j in 0..64 { assert_eq!(m1.get(i, j), m2.get(i, j)); } } } #[test] fn test_matrix_multiply() { let matrix = HeavyMatrix::new(); let input = [0x42u8; 32]; let output = matrix.multiply(&input); // Output should be deterministic let output2 = matrix.multiply(&input); assert_eq!(output, output2); // Output should be different from input assert_ne!(output, input); } #[test] fn test_optimized_matrix() { let matrix = HeavyMatrix::new(); let optimized = OptimizedMatrix::new(); let input = [0x55u8; 32]; let output1 = matrix.multiply(&input); let output2 = optimized.multiply(&input); // Both should produce same result assert_eq!(output1, output2); } #[test] fn test_gf_tables() { let tables = GfTables::new(); // Verify table matches direct computation for a in 0..=255u8 { for b in 0..=255u8 { assert_eq!(tables.mul(a, b), gf_mul(a, b)); } } } }