Skip to main content

pot_o_mining/
pot_o.rs

1//! PoT-O consensus: proof generation, verification, and engine stats.
2
3use crate::challenge::{Challenge, ChallengeGenerator};
4use crate::mml_path::MMLPathValidator;
5use crate::neural_path::NeuralPathValidator;
6use ai3_lib::{AI3Engine, EngineStats, TensorEngine};
7use pot_o_core::TribeResult;
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10use std::time::Instant;
11
12/// The full PoT-O proof submitted by a miner.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct PotOProof {
15    pub challenge_id: String,
16    pub challenge_hash: String,
17    pub tensor_result_hash: String,
18    pub mml_score: f64,
19    pub path_signature: String,
20    pub path_distance: u32,
21    pub computation_nonce: u64,
22    pub computation_hash: String,
23    pub miner_pubkey: String,
24    pub timestamp: chrono::DateTime<chrono::Utc>,
25}
26
27/// Payload sent to the on-chain program.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ProofPayload {
30    pub proof: PotOProof,
31    pub signature: Vec<u8>,
32}
33
34/// The PoT-O consensus engine. Orchestrates challenge generation,
35/// tensor computation, MML validation, and neural path matching.
36pub struct PotOConsensus {
37    pub engine: Box<dyn TensorEngine>,
38    pub challenge_gen: ChallengeGenerator,
39    pub mml_validator: MMLPathValidator,
40    pub neural_validator: NeuralPathValidator,
41}
42
43impl PotOConsensus {
44    pub fn new(difficulty: u64, max_tensor_dim: usize) -> Self {
45        Self {
46            engine: Box::new(AI3Engine::new()),
47            challenge_gen: ChallengeGenerator::new(difficulty, max_tensor_dim),
48            mml_validator: MMLPathValidator::default(),
49            neural_validator: NeuralPathValidator::default(),
50        }
51    }
52
53    /// Generate a new challenge from the latest Solana slot data.
54    pub fn generate_challenge(&self, slot: u64, slot_hash: &str) -> TribeResult<Challenge> {
55        self.challenge_gen.generate(slot, slot_hash)
56    }
57
58    /// Attempt to mine a proof for a given challenge. Iterates nonces until
59    /// both MML and neural-path constraints are satisfied, or max_iterations is hit.
60    pub fn mine(
61        &self,
62        challenge: &Challenge,
63        miner_pubkey: &str,
64        max_iterations: u64,
65    ) -> TribeResult<Option<PotOProof>> {
66        let start = Instant::now();
67        let task = challenge.to_mining_task(miner_pubkey);
68
69        let output_tensor = self.engine.execute_task(&task)?;
70        let mml_score = self
71            .mml_validator
72            .compute_mml_score(&challenge.input_tensor, &output_tensor)?;
73
74        for nonce in 0..max_iterations {
75            let actual_path = self
76                .neural_validator
77                .compute_actual_path(&output_tensor, nonce)?;
78            let expected = self.neural_validator.expected_path_signature(&challenge.id);
79            let min_len = actual_path.len().min(expected.len());
80            let distance = NeuralPathValidator::hamming_distance(
81                &actual_path[..min_len],
82                &expected[..min_len],
83            );
84
85            if distance <= challenge.path_distance_max
86                && self
87                    .mml_validator
88                    .validate(mml_score, challenge.mml_threshold)
89            {
90                let tensor_result_hash = output_tensor.calculate_hash();
91                let path_sig = NeuralPathValidator::path_to_hex(&actual_path);
92                let computation_hash = Self::compute_proof_hash(
93                    &challenge.id,
94                    &tensor_result_hash,
95                    mml_score,
96                    &path_sig,
97                    nonce,
98                );
99
100                let elapsed = start.elapsed();
101                self.engine.record_result(true, elapsed);
102
103                return Ok(Some(PotOProof {
104                    challenge_id: challenge.id.clone(),
105                    challenge_hash: challenge.slot_hash.clone(),
106                    tensor_result_hash,
107                    mml_score,
108                    path_signature: path_sig,
109                    path_distance: distance,
110                    computation_nonce: nonce,
111                    computation_hash,
112                    miner_pubkey: miner_pubkey.to_string(),
113                    timestamp: chrono::Utc::now(),
114                }));
115            }
116        }
117
118        self.engine.record_result(false, start.elapsed());
119        Ok(None)
120    }
121
122    /// Verify a proof offline (same checks the on-chain program performs).
123    pub fn verify_proof(&self, proof: &PotOProof, challenge: &Challenge) -> TribeResult<bool> {
124        // 1. Verify computation hash integrity
125        let expected_hash = Self::compute_proof_hash(
126            &proof.challenge_id,
127            &proof.tensor_result_hash,
128            proof.mml_score,
129            &proof.path_signature,
130            proof.computation_nonce,
131        );
132        if expected_hash != proof.computation_hash {
133            return Ok(false);
134        }
135
136        // 2. Verify MML score meets threshold
137        if !self
138            .mml_validator
139            .validate(proof.mml_score, challenge.mml_threshold)
140        {
141            return Ok(false);
142        }
143
144        // 3. Verify path distance
145        if proof.path_distance > challenge.path_distance_max {
146            return Ok(false);
147        }
148
149        Ok(true)
150    }
151
152    /// Expected path and calc counts for this challenge (for status dashboard treemap).
153    /// - expected_paths: length of the neural path signature (deterministic per challenge).
154    /// - expected_calcs: 1 + difficulty (one base tensor op plus difficulty-derived steps).
155    pub fn expected_paths_and_calcs(&self, challenge: &Challenge) -> (u64, u64) {
156        let expected_paths = self
157            .neural_validator
158            .expected_path_signature(&challenge.id)
159            .len() as u64;
160        let expected_calcs = 1 + challenge.difficulty;
161        (expected_paths, expected_calcs)
162    }
163
164    /// Expose a read-only view of engine stats via the TensorEngine abstraction.
165    pub fn engine_stats(&self) -> EngineStats {
166        self.engine.get_stats()
167    }
168
169    /// Compute the deterministic proof hash: sha256(challenge_id || tensor_hash || mml_score || path_sig || nonce)
170    pub fn compute_proof_hash(
171        challenge_id: &str,
172        tensor_result_hash: &str,
173        mml_score: f64,
174        path_signature: &str,
175        nonce: u64,
176    ) -> String {
177        let mut hasher = Sha256::new();
178        hasher.update(challenge_id.as_bytes());
179        hasher.update(tensor_result_hash.as_bytes());
180        hasher.update(mml_score.to_le_bytes());
181        hasher.update(path_signature.as_bytes());
182        hasher.update(nonce.to_le_bytes());
183        hex::encode(hasher.finalize())
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_consensus_lifecycle() {
193        let consensus = PotOConsensus::new(1, 8);
194        let hash = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
195        let challenge = consensus.generate_challenge(100, hash).unwrap();
196        assert!(!challenge.id.is_empty());
197
198        // With low difficulty and small tensors, mining should find a proof quickly
199        let result = consensus
200            .mine(&challenge, "test_miner_pubkey", 1000)
201            .unwrap();
202        assert!(result.is_some(), "Should find a proof with low difficulty");
203
204        let proof = result.unwrap();
205        let valid = consensus.verify_proof(&proof, &challenge).unwrap();
206        assert!(valid, "Mined proof should verify");
207    }
208
209    #[test]
210    fn test_proof_hash_deterministic() {
211        let h1 = PotOConsensus::compute_proof_hash("chal", "tensor", 0.5, "path", 42);
212        let h2 = PotOConsensus::compute_proof_hash("chal", "tensor", 0.5, "path", 42);
213        assert_eq!(h1, h2);
214    }
215
216    #[test]
217    fn test_expected_paths_and_calcs() {
218        let consensus = PotOConsensus::new(2, 8);
219        let hash = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
220        let challenge = consensus.generate_challenge(100, hash).unwrap();
221        let (expected_paths, expected_calcs) = consensus.expected_paths_and_calcs(&challenge);
222        assert!(expected_paths > 0, "expected_paths should be positive");
223        assert!(expected_calcs > 0, "expected_calcs should be positive");
224        let path_len = consensus
225            .neural_validator
226            .expected_path_signature(&challenge.id)
227            .len() as u64;
228        assert_eq!(
229            expected_paths, path_len,
230            "expected_paths should match path signature length"
231        );
232        assert_eq!(expected_calcs, 1 + challenge.difficulty);
233    }
234}