Skip to main content

pot_o_mining/
neural_path.rs

1//! Neural path validation: expected path signature from challenge and actual path from tensor + nonce.
2
3use ai3_lib::tensor::Tensor;
4use pot_o_core::TribeResult;
5use sha2::{Digest, Sha256};
6
7/// Validates the neural inference path of a tensor computation.
8///
9/// Models the tensor operation as a small feedforward network. The "path"
10/// is the binary activation pattern at each layer (ReLU > 0 = 1, else 0).
11/// The miner must find a nonce that makes the actual path match an expected
12/// path signature (derived from the challenge) within a Hamming distance.
13pub struct NeuralPathValidator {
14    /// Layer widths for the feedforward network
15    pub layer_widths: Vec<usize>,
16}
17
18impl Default for NeuralPathValidator {
19    fn default() -> Self {
20        Self {
21            layer_widths: vec![32, 16, 8],
22        }
23    }
24}
25
26impl NeuralPathValidator {
27    /// Derive the expected path signature from the challenge hash.
28    /// Returns a bit vector (as Vec<u8> of 0/1 values) representing the expected activations.
29    pub fn expected_path_signature(&self, challenge_hash: &str) -> Vec<u8> {
30        let hash_bytes = hex::decode(challenge_hash).unwrap_or_default();
31        let total_neurons: usize = self.layer_widths.iter().sum();
32        let mut sig = Vec::with_capacity(total_neurons);
33
34        let mut hasher = Sha256::new();
35        hasher.update(&hash_bytes);
36        let mut seed = hasher.finalize().to_vec();
37
38        for &width in &self.layer_widths {
39            for i in 0..width {
40                let byte_idx = i % seed.len();
41                let bit = (seed[byte_idx] >> (i % 8)) & 1;
42                sig.push(bit);
43            }
44            // Re-hash seed for next layer so each layer has different expected bits
45            let mut h = Sha256::new();
46            h.update(&seed);
47            seed = h.finalize().to_vec();
48        }
49
50        sig
51    }
52
53    /// Compute the actual activation path for a tensor with a given nonce.
54    /// Simulates a feedforward pass: input -> (linear + ReLU) per layer.
55    /// Returns the binary activation pattern.
56    pub fn compute_actual_path(&self, tensor: &Tensor, nonce: u64) -> TribeResult<Vec<u8>> {
57        let mut activations = tensor.data.as_f32();
58        let mut path_bits = Vec::new();
59        let mut bit_idx: u32 = 0;
60
61        for &width in &self.layer_widths {
62            let mut layer_output = vec![0.0f32; width];
63
64            // Simplified linear: each output neuron sums a stride of the input
65            let stride = (activations.len() / width).max(1);
66            for (j, out) in layer_output.iter_mut().enumerate() {
67                if j >= width {
68                    break;
69                }
70                let start = j * stride;
71                let end = (start + stride).min(activations.len());
72                let sum: f32 = activations[start..end].iter().sum();
73                // ReLU
74                let relu = sum.max(0.0);
75                *out = relu;
76
77                let base_bit = if relu > 0.0 { 1u8 } else { 0u8 };
78                let shift = (bit_idx % 64) as u64;
79                let nonce_bit = ((nonce >> shift) & 1) as u8;
80                let bit = base_bit ^ nonce_bit;
81
82                path_bits.push(bit);
83                bit_idx = bit_idx.wrapping_add(1);
84            }
85
86            activations = layer_output;
87        }
88
89        Ok(path_bits)
90    }
91
92    /// Compute Hamming distance between two bit vectors.
93    pub fn hamming_distance(a: &[u8], b: &[u8]) -> u32 {
94        a.iter()
95            .zip(b.iter())
96            .map(|(&x, &y)| if x != y { 1u32 } else { 0u32 })
97            .sum()
98    }
99
100    /// Validate that the actual path is close enough to the expected path.
101    pub fn validate(&self, actual_path: &[u8], challenge_hash: &str, max_distance: u32) -> bool {
102        let expected = self.expected_path_signature(challenge_hash);
103        let min_len = actual_path.len().min(expected.len());
104        let distance = Self::hamming_distance(&actual_path[..min_len], &expected[..min_len]);
105        distance <= max_distance
106    }
107
108    /// Encode path bits as a compact hex string for on-chain storage.
109    pub fn path_to_hex(path: &[u8]) -> String {
110        let mut bytes = Vec::with_capacity(path.len().div_ceil(8));
111        for chunk in path.chunks(8) {
112            let mut byte = 0u8;
113            for (i, &bit) in chunk.iter().enumerate() {
114                if bit != 0 {
115                    byte |= 1 << i;
116                }
117            }
118            bytes.push(byte);
119        }
120        hex::encode(bytes)
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use ai3_lib::tensor::{TensorData, TensorShape};
128
129    #[test]
130    fn test_expected_path_deterministic() {
131        let v = NeuralPathValidator::default();
132        let hash = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
133        let p1 = v.expected_path_signature(hash);
134        let p2 = v.expected_path_signature(hash);
135        assert_eq!(p1, p2);
136    }
137
138    #[test]
139    fn test_hamming_distance() {
140        assert_eq!(
141            NeuralPathValidator::hamming_distance(&[0, 1, 0], &[0, 1, 0]),
142            0
143        );
144        assert_eq!(
145            NeuralPathValidator::hamming_distance(&[0, 1, 0], &[1, 0, 1]),
146            3
147        );
148        assert_eq!(
149            NeuralPathValidator::hamming_distance(&[1, 1, 1], &[0, 1, 0]),
150            2
151        );
152    }
153
154    #[test]
155    fn test_actual_path_varies_with_nonce() {
156        let v = NeuralPathValidator::default();
157        let t = Tensor::new(TensorShape::new(vec![64]), TensorData::F32(vec![0.5; 64])).unwrap();
158        let p1 = v.compute_actual_path(&t, 0).unwrap();
159        let p2 = v.compute_actual_path(&t, 999_999).unwrap();
160        // Different nonces should (usually) produce different paths
161        assert_ne!(p1, p2);
162    }
163
164    #[test]
165    fn test_path_hex_roundtrip() {
166        let path = vec![1, 0, 1, 1, 0, 0, 1, 0, 1];
167        let hex_str = NeuralPathValidator::path_to_hex(&path);
168        assert!(!hex_str.is_empty());
169    }
170}