Skip to main content

pot_o_mining/
challenge.rs

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