Skip to main content

pot_o_mining/
challenge.rs

1//! Challenge generation from slot/slot_hash and conversion to mining tasks.
2
3use crate::neural_path::NeuralPathValidator;
4use ai3_lib::tensor::{Tensor, TensorData, TensorShape};
5use ai3_lib::MiningTask;
6use pot_o_core::TribeResult;
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9
10/// A PoT-O mining challenge derived from a Solana slot hash.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Challenge {
13    /// Unique challenge id.
14    pub id: String,
15    /// Solana slot number.
16    pub slot: u64,
17    /// Slot hash (e.g. 64-char hex).
18    pub slot_hash: String,
19    /// Tensor operation type (e.g. matrix_multiply, relu).
20    pub operation_type: String,
21    /// Input tensor for the operation.
22    pub input_tensor: Tensor,
23    /// Mining difficulty (path distance, etc.).
24    pub difficulty: u64,
25    /// MML score threshold to accept a proof.
26    pub mml_threshold: f64,
27    /// Maximum allowed Hamming distance for neural path.
28    pub path_distance_max: u32,
29    /// Max tensor dimension.
30    pub max_tensor_dim: usize,
31    /// Creation time.
32    pub created_at: chrono::DateTime<chrono::Utc>,
33    /// Expiry time.
34    pub expires_at: chrono::DateTime<chrono::Utc>,
35}
36
37impl Challenge {
38    /// Returns true if the challenge has expired.
39    pub fn is_expired(&self) -> bool {
40        chrono::Utc::now() > self.expires_at
41    }
42
43    /// Converts this challenge into an AI3 mining task for the given requester.
44    pub fn to_mining_task(&self, requester: &str) -> MiningTask {
45        MiningTask::new(
46            self.operation_type.clone(),
47            vec![self.input_tensor.clone()],
48            self.difficulty,
49            50_000_000, // 50 TRIBE reward
50            300,
51            requester.to_string(),
52        )
53    }
54}
55
56/// Generates deterministic challenges from (slot, slot_hash) with configurable difficulty and thresholds.
57pub struct ChallengeGenerator {
58    /// Base difficulty for generated challenges.
59    pub base_difficulty: u64,
60    /// Base MML threshold.
61    pub base_mml_threshold: f64,
62    /// Base path distance bound.
63    pub base_path_distance: u32,
64    /// Maximum tensor dimension.
65    pub max_tensor_dim: usize,
66    /// Challenge TTL in seconds.
67    pub challenge_ttl_secs: i64,
68}
69
70impl Default for ChallengeGenerator {
71    fn default() -> Self {
72        let base_path_distance = NeuralPathValidator::default()
73            .layer_widths
74            .iter()
75            .sum::<usize>() as u32;
76
77        Self {
78            base_difficulty: 2,
79            base_mml_threshold: 2.0,
80            base_path_distance,
81            max_tensor_dim: pot_o_core::ESP_MAX_TENSOR_DIM,
82            challenge_ttl_secs: 120,
83        }
84    }
85}
86
87/// Available operations weighted by slot hash byte for deterministic selection.
88const OPERATIONS: &[&str] = &[
89    "matrix_multiply",
90    "convolution",
91    "relu",
92    "sigmoid",
93    "tanh",
94    "dot_product",
95    "normalize",
96];
97
98impl ChallengeGenerator {
99    /// Creates a generator with the given difficulty and max tensor dimension.
100    pub fn new(difficulty: u64, max_tensor_dim: usize) -> Self {
101        Self {
102            base_difficulty: difficulty,
103            max_tensor_dim,
104            ..Default::default()
105        }
106    }
107
108    /// Derive a deterministic challenge from a Solana slot hash.
109    pub fn generate(&self, slot: u64, slot_hash_hex: &str) -> TribeResult<Challenge> {
110        let hash_bytes = hex::decode(slot_hash_hex).map_err(|e| {
111            pot_o_core::TribeError::InvalidOperation(format!("Invalid slot hash hex: {e}"))
112        })?;
113
114        let op_index = hash_bytes.first().copied().unwrap_or(0) as usize % OPERATIONS.len();
115        let operation_type = OPERATIONS[op_index].to_string();
116
117        let input_tensor = self.derive_input_tensor(&hash_bytes)?;
118
119        let difficulty = self.compute_difficulty(slot);
120        let mml_threshold = self.base_mml_threshold / (1.0 + (difficulty as f64).log2().max(0.0));
121        let path_distance_max = self
122            .base_path_distance
123            .saturating_sub((difficulty as u32).min(self.base_path_distance - 1));
124
125        let now = chrono::Utc::now();
126        let challenge_id = {
127            let mut h = Sha256::new();
128            h.update(slot.to_le_bytes());
129            h.update(&hash_bytes);
130            hex::encode(h.finalize())
131        };
132
133        Ok(Challenge {
134            id: challenge_id,
135            slot,
136            slot_hash: slot_hash_hex.to_string(),
137            operation_type,
138            input_tensor,
139            difficulty,
140            mml_threshold,
141            path_distance_max,
142            max_tensor_dim: self.max_tensor_dim,
143            created_at: now,
144            expires_at: now + chrono::Duration::seconds(self.challenge_ttl_secs),
145        })
146    }
147
148    /// Build an input tensor from slot hash bytes. Dimensions are clamped to max_tensor_dim.
149    fn derive_input_tensor(&self, hash_bytes: &[u8]) -> TribeResult<Tensor> {
150        let dim_byte = hash_bytes.get(1).copied().unwrap_or(4);
151        let dim = ((dim_byte as usize % self.max_tensor_dim) + 2).min(self.max_tensor_dim);
152        let total = dim * dim;
153
154        let mut floats: Vec<f32> = hash_bytes.iter().map(|&b| b as f32 / 255.0).collect();
155        // Extend deterministically if hash is shorter than needed
156        while floats.len() < total {
157            let seed = floats.len() as f32 * 0.618_034;
158            floats.push(seed.fract());
159        }
160        floats.truncate(total);
161
162        Tensor::new(TensorShape::new(vec![dim, dim]), TensorData::F32(floats))
163    }
164
165    /// Difficulty scales with slot height (gradual increase).
166    fn compute_difficulty(&self, slot: u64) -> u64 {
167        let epoch = slot / 10_000;
168        self.base_difficulty + epoch.min(10)
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_challenge_generation() {
178        let gen = ChallengeGenerator::default();
179        let hash = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
180        let challenge = gen.generate(100, hash).unwrap();
181        assert!(!challenge.id.is_empty());
182        assert!(challenge.mml_threshold > 0.0);
183        assert!(challenge.mml_threshold <= gen.base_mml_threshold);
184    }
185
186    #[test]
187    fn test_deterministic_operation() {
188        let gen = ChallengeGenerator::default();
189        let hash = "ff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";
190        let c1 = gen.generate(42, hash).unwrap();
191        let c2 = gen.generate(42, hash).unwrap();
192        assert_eq!(c1.operation_type, c2.operation_type);
193        assert_eq!(c1.id, c2.id);
194    }
195}