Skip to main content

pot_o_extensions/
chain_bridge.rs

1//! Chain bridge: submit proofs and query miners on Solana (and optional EVM/cross-chain).
2
3use async_trait::async_trait;
4use borsh::{BorshDeserialize, BorshSerialize};
5use pot_o_core::{TribeError, TribeResult};
6use pot_o_mining::ProofPayload;
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9use solana_client::rpc_client::RpcClient;
10use solana_sdk::{
11    instruction::{AccountMeta, Instruction},
12    pubkey::Pubkey,
13    signature::{read_keypair_file, Keypair, Signer as SolSigner},
14    system_program,
15    transaction::Transaction,
16};
17use std::str::FromStr;
18use std::sync::Arc;
19
20// ---------------------------------------------------------------------------
21// Types
22// ---------------------------------------------------------------------------
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct TxSignature(pub String);
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct MinerAccount {
29    pub pubkey: String,
30    pub total_proofs: u64,
31    pub total_rewards: u64,
32    pub pending_rewards: u64,
33    pub reputation_score: f64,
34    pub last_proof_slot: u64,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38pub enum Token {
39    SOL,
40    PTtC,
41    NMTC,
42}
43
44// ---------------------------------------------------------------------------
45// Trait
46// ---------------------------------------------------------------------------
47
48/// How proofs and rewards interact with on-chain programs.
49#[async_trait]
50pub trait ChainBridge: Send + Sync {
51    async fn submit_proof(&self, proof: &ProofPayload) -> TribeResult<TxSignature>;
52    async fn query_miner(&self, pubkey: &str) -> TribeResult<Option<MinerAccount>>;
53    /// Register a miner on-chain (creates MinerAccount PDA). Used for auto-registration of new devices/miners.
54    async fn register_miner(&self, miner_pubkey: &str) -> TribeResult<TxSignature>;
55    async fn get_current_difficulty(&self) -> TribeResult<u64>;
56    async fn request_swap(
57        &self,
58        from_token: Token,
59        to_token: Token,
60        amount: u64,
61    ) -> TribeResult<TxSignature>;
62}
63
64// ---------------------------------------------------------------------------
65// On-chain ProofParams mirror (Borsh-serialized for Anchor IX)
66// ---------------------------------------------------------------------------
67
68#[derive(BorshSerialize)]
69struct OnChainProofParams {
70    challenge_id: [u8; 32],
71    challenge_slot: u64,
72    tensor_result_hash: [u8; 32],
73    mml_score: u64,
74    path_signature: [u8; 32],
75    path_distance: u32,
76    computation_nonce: u64,
77    computation_hash: [u8; 32],
78}
79
80const MML_SCALE: f64 = 1_000_000_000.0;
81
82/// On-chain MinerAccount layout (Borsh). Must match the tribewarez-pot-o program.
83/// Skip 8-byte Anchor account discriminator before deserializing.
84#[derive(BorshDeserialize)]
85struct OnChainMinerAccount {
86    owner: Pubkey,
87    total_proofs: u64,
88    total_rewards: u64,
89    pending_rewards: u64,
90    reputation_score: f64,
91    last_proof_slot: u64,
92}
93
94fn hex_to_32(hex_str: &str) -> Result<[u8; 32], TribeError> {
95    let bytes = hex::decode(hex_str)
96        .map_err(|e| TribeError::ChainBridgeError(format!("hex decode: {e}")))?;
97    bytes
98        .try_into()
99        .map_err(|_| TribeError::ChainBridgeError("expected 32 bytes from hex".into()))
100}
101
102fn anchor_discriminator(name: &str) -> [u8; 8] {
103    let mut hasher = Sha256::new();
104    hasher.update(format!("global:{name}"));
105    let hash = hasher.finalize();
106    let mut disc = [0u8; 8];
107    disc.copy_from_slice(&hash[..8]);
108    disc
109}
110
111// ---------------------------------------------------------------------------
112// SolanaBridge
113// ---------------------------------------------------------------------------
114
115pub struct SolanaBridge {
116    pub rpc_url: String,
117    pub program_id: Pubkey,
118    relayer_keypair: Option<Arc<Keypair>>,
119    /// When true, submit_proof will call register_miner for unknown miners before submitting.
120    auto_register_miners: bool,
121}
122
123impl SolanaBridge {
124    pub fn new(
125        rpc_url: String,
126        program_id: String,
127        keypair_path: String,
128        auto_register_miners: bool,
129    ) -> Self {
130        let pid = Pubkey::from_str(&program_id).unwrap_or_else(|e| {
131            tracing::warn!(error = %e, program_id, "Invalid program_id, using default");
132            Pubkey::default()
133        });
134
135        let kp = match read_keypair_file(&keypair_path) {
136            Ok(k) => {
137                tracing::info!(
138                    relayer = %k.pubkey(),
139                    path = %keypair_path,
140                    "Loaded relayer keypair"
141                );
142                Some(Arc::new(k))
143            }
144            Err(e) => {
145                tracing::warn!(
146                    error = %e,
147                    path = %keypair_path,
148                    "Relayer keypair not found; on-chain submissions will return stub signatures"
149                );
150                None
151            }
152        };
153
154        Self {
155            rpc_url,
156            program_id: pid,
157            relayer_keypair: kp,
158            auto_register_miners,
159        }
160    }
161
162    fn build_register_miner_ix(&self, miner_pubkey: &Pubkey) -> TribeResult<Instruction> {
163        let relayer_pubkey = self
164            .relayer_keypair
165            .as_ref()
166            .expect("checked before calling")
167            .pubkey();
168
169        let (config_pda, _) = Pubkey::find_program_address(&[b"pot_o_config"], &self.program_id);
170        let (miner_pda, _) =
171            Pubkey::find_program_address(&[b"miner", miner_pubkey.as_ref()], &self.program_id);
172
173        let disc = anchor_discriminator("register_miner");
174        let data = disc.to_vec();
175
176        let accounts = vec![
177            AccountMeta::new_readonly(config_pda, false),
178            AccountMeta::new_readonly(*miner_pubkey, false),
179            AccountMeta::new(miner_pda, false),
180            AccountMeta::new(relayer_pubkey, true),
181            AccountMeta::new_readonly(system_program::ID, false),
182        ];
183
184        Ok(Instruction {
185            program_id: self.program_id,
186            accounts,
187            data,
188        })
189    }
190
191    fn build_submit_proof_ix(
192        &self,
193        proof: &ProofPayload,
194        challenge_slot: u64,
195    ) -> TribeResult<Instruction> {
196        let miner_pubkey = Pubkey::from_str(&proof.proof.miner_pubkey)
197            .map_err(|e| TribeError::ChainBridgeError(format!("invalid miner pubkey: {e}")))?;
198
199        let relayer_pubkey = self
200            .relayer_keypair
201            .as_ref()
202            .expect("checked before calling")
203            .pubkey();
204
205        let (config_pda, _) = Pubkey::find_program_address(&[b"pot_o_config"], &self.program_id);
206        let (miner_pda, _) =
207            Pubkey::find_program_address(&[b"miner", miner_pubkey.as_ref()], &self.program_id);
208
209        let challenge_id_bytes = hex_to_32(&proof.proof.challenge_id)?;
210        let (proof_pda, _) = Pubkey::find_program_address(
211            &[b"proof", challenge_id_bytes.as_ref()],
212            &self.program_id,
213        );
214
215        let params = OnChainProofParams {
216            challenge_id: challenge_id_bytes,
217            challenge_slot,
218            tensor_result_hash: hex_to_32(&proof.proof.tensor_result_hash)?,
219            mml_score: (proof.proof.mml_score * MML_SCALE) as u64,
220            path_signature: hex_to_32(&proof.proof.path_signature)?,
221            path_distance: proof.proof.path_distance,
222            computation_nonce: proof.proof.computation_nonce,
223            computation_hash: hex_to_32(&proof.proof.computation_hash)?,
224        };
225
226        let disc = anchor_discriminator("submit_proof");
227        let params_data = borsh::to_vec(&params)
228            .map_err(|e| TribeError::ChainBridgeError(format!("borsh serialize: {e}")))?;
229        let mut data = Vec::with_capacity(8 + params_data.len());
230        data.extend_from_slice(&disc);
231        data.extend_from_slice(&params_data);
232
233        let accounts = vec![
234            AccountMeta::new(config_pda, false),
235            AccountMeta::new_readonly(miner_pubkey, false),
236            AccountMeta::new(miner_pda, false),
237            AccountMeta::new(proof_pda, false),
238            AccountMeta::new(relayer_pubkey, true),
239            AccountMeta::new_readonly(system_program::ID, false),
240        ];
241
242        Ok(Instruction {
243            program_id: self.program_id,
244            accounts,
245            data,
246        })
247    }
248
249    fn stub_signature(proof: &ProofPayload) -> TxSignature {
250        let hash = &proof.proof.computation_hash;
251        let suffix = if hash.len() >= 16 { &hash[..16] } else { hash };
252        TxSignature(format!("sim_tx_{suffix}"))
253    }
254}
255
256#[async_trait]
257impl ChainBridge for SolanaBridge {
258    async fn submit_proof(&self, proof: &ProofPayload) -> TribeResult<TxSignature> {
259        let kp = match &self.relayer_keypair {
260            Some(k) => Arc::clone(k),
261            None => {
262                tracing::warn!(
263                    challenge = %proof.proof.challenge_id,
264                    miner = %proof.proof.miner_pubkey,
265                    "No relayer keypair; returning stub signature"
266                );
267                return Ok(Self::stub_signature(proof));
268            }
269        };
270
271        if self.auto_register_miners {
272            if let Ok(None) = self.query_miner(&proof.proof.miner_pubkey).await {
273                tracing::info!(
274                    miner = %proof.proof.miner_pubkey,
275                    "Miner not on-chain; auto-registering before submit"
276                );
277                if let Err(e) = self.register_miner(&proof.proof.miner_pubkey).await {
278                    tracing::warn!(
279                        miner = %proof.proof.miner_pubkey,
280                        error = %e,
281                        "Auto-register failed (may already exist); continuing with submit"
282                    );
283                    // Continue anyway: program may reject if miner still missing, or idempotent
284                }
285            }
286        }
287
288        tracing::info!(
289            challenge = %proof.proof.challenge_id,
290            miner = %proof.proof.miner_pubkey,
291            program = %self.program_id,
292            relayer = %kp.pubkey(),
293            "Submitting proof to Solana"
294        );
295
296        let rpc_url = self.rpc_url.clone();
297        let proof_clone = proof.clone();
298
299        let rpc_url_slot = rpc_url.clone();
300        let challenge_slot = tokio::task::spawn_blocking(move || {
301            let client = RpcClient::new(&rpc_url_slot);
302            client.get_slot().map_err(Box::new)
303        })
304        .await
305        .map_err(|e| TribeError::ChainBridgeError(format!("spawn_blocking join: {e}")))?
306        .map_err(|e| TribeError::ChainBridgeError(format!("get_slot: {e}")))?;
307
308        tracing::debug!(challenge_slot, "Fetched current Solana slot");
309
310        let ix = self.build_submit_proof_ix(&proof_clone, challenge_slot)?;
311
312        let sig = tokio::task::spawn_blocking(move || -> TribeResult<String> {
313            let client = RpcClient::new(&rpc_url);
314            let blockhash = client
315                .get_latest_blockhash()
316                .map_err(|e| TribeError::ChainBridgeError(format!("get_latest_blockhash: {e}")))?;
317
318            let tx =
319                Transaction::new_signed_with_payer(&[ix], Some(&kp.pubkey()), &[&kp], blockhash);
320
321            let signature = client
322                .send_and_confirm_transaction(&tx)
323                .map_err(|e| TribeError::ChainBridgeError(format!("send_and_confirm: {e}")))?;
324
325            Ok(signature.to_string())
326        })
327        .await
328        .map_err(|e| TribeError::ChainBridgeError(format!("spawn_blocking join: {e}")))??;
329
330        tracing::info!(tx_signature = %sig, "Proof submitted to Solana successfully");
331        Ok(TxSignature(sig))
332    }
333
334    async fn query_miner(&self, pubkey: &str) -> TribeResult<Option<MinerAccount>> {
335        let miner_pubkey = Pubkey::from_str(pubkey)
336            .map_err(|e| TribeError::ChainBridgeError(format!("invalid miner pubkey: {e}")))?;
337        let (miner_pda, _) =
338            Pubkey::find_program_address(&[b"miner", miner_pubkey.as_ref()], &self.program_id);
339
340        let rpc_url = self.rpc_url.clone();
341        let _program_id = self.program_id;
342
343        let result = tokio::task::spawn_blocking(move || -> TribeResult<Option<MinerAccount>> {
344            let client = RpcClient::new(&rpc_url);
345            match client.get_account(&miner_pda) {
346                Ok(account) => {
347                    let data = account.data;
348                    if data.len() < 8 {
349                        return Ok(None);
350                    }
351                    let payload = &data[8..];
352                    match OnChainMinerAccount::try_from_slice(payload) {
353                        Ok(on_chain) => Ok(Some(MinerAccount {
354                            pubkey: on_chain.owner.to_string(),
355                            total_proofs: on_chain.total_proofs,
356                            total_rewards: on_chain.total_rewards,
357                            pending_rewards: on_chain.pending_rewards,
358                            reputation_score: on_chain.reputation_score,
359                            last_proof_slot: on_chain.last_proof_slot,
360                        })),
361                        Err(e) => {
362                            tracing::debug!(error = %e, "Failed to deserialize miner account");
363                            Ok(None)
364                        }
365                    }
366                }
367                Err(_) => Ok(None),
368            }
369        })
370        .await
371        .map_err(|e| TribeError::ChainBridgeError(format!("spawn_blocking join: {e}")))??;
372
373        tracing::debug!(
374            pubkey,
375            found = result.is_some(),
376            "Querying miner account on-chain"
377        );
378        Ok(result)
379    }
380
381    async fn register_miner(&self, miner_pubkey: &str) -> TribeResult<TxSignature> {
382        let kp = match &self.relayer_keypair {
383            Some(k) => Arc::clone(k),
384            None => {
385                return Err(TribeError::ChainBridgeError(
386                    "No relayer keypair; cannot register miner".into(),
387                ));
388            }
389        };
390
391        let miner_pubkey = Pubkey::from_str(miner_pubkey)
392            .map_err(|e| TribeError::ChainBridgeError(format!("invalid miner pubkey: {e}")))?;
393        let ix = self.build_register_miner_ix(&miner_pubkey)?;
394
395        let rpc_url = self.rpc_url.clone();
396
397        let sig = tokio::task::spawn_blocking(move || -> TribeResult<String> {
398            let client = RpcClient::new(&rpc_url);
399            let blockhash = client
400                .get_latest_blockhash()
401                .map_err(|e| TribeError::ChainBridgeError(format!("get_latest_blockhash: {e}")))?;
402
403            let tx =
404                Transaction::new_signed_with_payer(&[ix], Some(&kp.pubkey()), &[&kp], blockhash);
405
406            match client.send_and_confirm_transaction(&tx) {
407                Ok(signature) => {
408                    tracing::info!(
409                        miner = %miner_pubkey,
410                        tx_signature = %signature,
411                        "Miner registered on-chain"
412                    );
413                    Ok(signature.to_string())
414                }
415                Err(e) => {
416                    let err_str = e.to_string();
417                    if err_str.contains("already in use") || err_str.contains("already exists") {
418                        tracing::debug!(
419                            miner = %miner_pubkey,
420                            "Miner account already exists (idempotent)"
421                        );
422                        Ok(format!("already_registered_{}", miner_pubkey))
423                    } else {
424                        Err(TribeError::ChainBridgeError(format!(
425                            "register_miner send_and_confirm: {e}"
426                        )))
427                    }
428                }
429            }
430        })
431        .await
432        .map_err(|e| TribeError::ChainBridgeError(format!("spawn_blocking join: {e}")))??;
433
434        Ok(TxSignature(sig))
435    }
436
437    async fn get_current_difficulty(&self) -> TribeResult<u64> {
438        Ok(2)
439    }
440
441    async fn request_swap(
442        &self,
443        from_token: Token,
444        to_token: Token,
445        amount: u64,
446    ) -> TribeResult<TxSignature> {
447        tracing::info!(
448            ?from_token,
449            ?to_token,
450            amount,
451            "Swap request (CPI to tribewarez-swap)"
452        );
453        Ok(TxSignature("sim_swap_placeholder".into()))
454    }
455}
456
457// ---------------------------------------------------------------------------
458// EvmBridge (stubbed)
459// ---------------------------------------------------------------------------
460
461pub struct EvmBridge {
462    pub rpc_url: String,
463    pub contract_address: String,
464}
465
466#[async_trait]
467impl ChainBridge for EvmBridge {
468    async fn submit_proof(&self, _proof: &ProofPayload) -> TribeResult<TxSignature> {
469        todo!("EVM proof submission not yet implemented")
470    }
471    async fn query_miner(&self, _pubkey: &str) -> TribeResult<Option<MinerAccount>> {
472        todo!("EVM miner query not yet implemented")
473    }
474    async fn register_miner(&self, _miner_pubkey: &str) -> TribeResult<TxSignature> {
475        todo!("EVM miner registration not yet implemented")
476    }
477    async fn get_current_difficulty(&self) -> TribeResult<u64> {
478        todo!("EVM difficulty query not yet implemented")
479    }
480    async fn request_swap(
481        &self,
482        _from: Token,
483        _to: Token,
484        _amount: u64,
485    ) -> TribeResult<TxSignature> {
486        todo!("EVM swap not yet implemented")
487    }
488}
489
490// ---------------------------------------------------------------------------
491// CrossChainBridge (stubbed)
492// ---------------------------------------------------------------------------
493
494pub struct CrossChainBridge {
495    pub solana_rpc_url: String,
496    pub evm_rpc_url: String,
497}
498
499#[async_trait]
500impl ChainBridge for CrossChainBridge {
501    async fn submit_proof(&self, _proof: &ProofPayload) -> TribeResult<TxSignature> {
502        todo!("Cross-chain proof submission not yet implemented")
503    }
504    async fn query_miner(&self, _pubkey: &str) -> TribeResult<Option<MinerAccount>> {
505        todo!("Cross-chain miner query not yet implemented")
506    }
507    async fn register_miner(&self, _miner_pubkey: &str) -> TribeResult<TxSignature> {
508        todo!("Cross-chain miner registration not yet implemented")
509    }
510    async fn get_current_difficulty(&self) -> TribeResult<u64> {
511        todo!("Cross-chain difficulty query not yet implemented")
512    }
513    async fn request_swap(
514        &self,
515        _from: Token,
516        _to: Token,
517        _amount: u64,
518    ) -> TribeResult<TxSignature> {
519        todo!("Cross-chain atomic swap not yet implemented")
520    }
521}