1use 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#[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#[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 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#[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#[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
111pub struct SolanaBridge {
116 pub rpc_url: String,
117 pub program_id: Pubkey,
118 relayer_keypair: Option<Arc<Keypair>>,
119 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(¶ms)
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(¶ms_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 }
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
457pub 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
490pub 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}