use async_trait::async_trait;
use borsh::{BorshDeserialize, BorshSerialize};
use pot_o_core::{TribeError, TribeResult};
use pot_o_mining::ProofPayload;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
signature::{read_keypair_file, Keypair, Signer as SolSigner},
system_program,
transaction::Transaction,
};
use std::str::FromStr;
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxSignature(pub String);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MinerAccount {
pub pubkey: String,
pub total_proofs: u64,
pub total_rewards: u64,
pub pending_rewards: u64,
pub reputation_score: f64,
pub last_proof_slot: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Token {
SOL,
PTtC,
NMTC,
}
#[async_trait]
pub trait ChainBridge: Send + Sync {
async fn submit_proof(&self, proof: &ProofPayload) -> TribeResult<TxSignature>;
async fn query_miner(&self, pubkey: &str) -> TribeResult<Option<MinerAccount>>;
async fn register_miner(&self, miner_pubkey: &str) -> TribeResult<TxSignature>;
async fn get_current_difficulty(&self) -> TribeResult<u64>;
async fn request_swap(
&self,
from_token: Token,
to_token: Token,
amount: u64,
) -> TribeResult<TxSignature>;
}
#[derive(BorshSerialize)]
struct OnChainProofParams {
challenge_id: [u8; 32],
challenge_slot: u64,
tensor_result_hash: [u8; 32],
mml_score: u64,
path_signature: [u8; 32],
path_distance: u32,
computation_nonce: u64,
computation_hash: [u8; 32],
}
const MML_SCALE: f64 = 1_000_000_000.0;
#[derive(BorshDeserialize)]
struct OnChainMinerAccount {
owner: Pubkey,
total_proofs: u64,
total_rewards: u64,
pending_rewards: u64,
reputation_score: f64,
last_proof_slot: u64,
}
fn hex_to_32(hex_str: &str) -> Result<[u8; 32], TribeError> {
let bytes = hex::decode(hex_str)
.map_err(|e| TribeError::ChainBridgeError(format!("hex decode: {e}")))?;
bytes
.try_into()
.map_err(|_| TribeError::ChainBridgeError("expected 32 bytes from hex".into()))
}
fn anchor_discriminator(name: &str) -> [u8; 8] {
let mut hasher = Sha256::new();
hasher.update(format!("global:{name}"));
let hash = hasher.finalize();
let mut disc = [0u8; 8];
disc.copy_from_slice(&hash[..8]);
disc
}
pub struct SolanaBridge {
pub rpc_url: String,
pub program_id: Pubkey,
relayer_keypair: Option<Arc<Keypair>>,
auto_register_miners: bool,
}
impl SolanaBridge {
pub fn new(
rpc_url: String,
program_id: String,
keypair_path: String,
auto_register_miners: bool,
) -> Self {
let pid = Pubkey::from_str(&program_id).unwrap_or_else(|e| {
tracing::warn!(error = %e, program_id, "Invalid program_id, using default");
Pubkey::default()
});
let kp = match read_keypair_file(&keypair_path) {
Ok(k) => {
tracing::info!(
relayer = %k.pubkey(),
path = %keypair_path,
"Loaded relayer keypair"
);
Some(Arc::new(k))
}
Err(e) => {
tracing::warn!(
error = %e,
path = %keypair_path,
"Relayer keypair not found; on-chain submissions will return stub signatures"
);
None
}
};
Self {
rpc_url,
program_id: pid,
relayer_keypair: kp,
auto_register_miners,
}
}
fn build_register_miner_ix(&self, miner_pubkey: &Pubkey) -> TribeResult<Instruction> {
let relayer_pubkey = self
.relayer_keypair
.as_ref()
.expect("checked before calling")
.pubkey();
let (config_pda, _) = Pubkey::find_program_address(&[b"pot_o_config"], &self.program_id);
let (miner_pda, _) =
Pubkey::find_program_address(&[b"miner", miner_pubkey.as_ref()], &self.program_id);
let disc = anchor_discriminator("register_miner");
let data = disc.to_vec();
let accounts = vec![
AccountMeta::new_readonly(config_pda, false),
AccountMeta::new_readonly(*miner_pubkey, false),
AccountMeta::new(miner_pda, false),
AccountMeta::new(relayer_pubkey, true),
AccountMeta::new_readonly(system_program::ID, false),
];
Ok(Instruction {
program_id: self.program_id,
accounts,
data,
})
}
fn build_submit_proof_ix(
&self,
proof: &ProofPayload,
challenge_slot: u64,
) -> TribeResult<Instruction> {
let miner_pubkey = Pubkey::from_str(&proof.proof.miner_pubkey)
.map_err(|e| TribeError::ChainBridgeError(format!("invalid miner pubkey: {e}")))?;
let relayer_pubkey = self
.relayer_keypair
.as_ref()
.expect("checked before calling")
.pubkey();
let (config_pda, _) = Pubkey::find_program_address(&[b"pot_o_config"], &self.program_id);
let (miner_pda, _) =
Pubkey::find_program_address(&[b"miner", miner_pubkey.as_ref()], &self.program_id);
let challenge_id_bytes = hex_to_32(&proof.proof.challenge_id)?;
let (proof_pda, _) = Pubkey::find_program_address(
&[b"proof", challenge_id_bytes.as_ref()],
&self.program_id,
);
let params = OnChainProofParams {
challenge_id: challenge_id_bytes,
challenge_slot,
tensor_result_hash: hex_to_32(&proof.proof.tensor_result_hash)?,
mml_score: (proof.proof.mml_score * MML_SCALE) as u64,
path_signature: hex_to_32(&proof.proof.path_signature)?,
path_distance: proof.proof.path_distance,
computation_nonce: proof.proof.computation_nonce,
computation_hash: hex_to_32(&proof.proof.computation_hash)?,
};
let disc = anchor_discriminator("submit_proof");
let params_data = borsh::to_vec(¶ms)
.map_err(|e| TribeError::ChainBridgeError(format!("borsh serialize: {e}")))?;
let mut data = Vec::with_capacity(8 + params_data.len());
data.extend_from_slice(&disc);
data.extend_from_slice(¶ms_data);
let accounts = vec![
AccountMeta::new(config_pda, false),
AccountMeta::new_readonly(miner_pubkey, false),
AccountMeta::new(miner_pda, false),
AccountMeta::new(proof_pda, false),
AccountMeta::new(relayer_pubkey, true),
AccountMeta::new_readonly(system_program::ID, false),
];
Ok(Instruction {
program_id: self.program_id,
accounts,
data,
})
}
fn stub_signature(proof: &ProofPayload) -> TxSignature {
let hash = &proof.proof.computation_hash;
let suffix = if hash.len() >= 16 { &hash[..16] } else { hash };
TxSignature(format!("sim_tx_{suffix}"))
}
}
#[async_trait]
impl ChainBridge for SolanaBridge {
async fn submit_proof(&self, proof: &ProofPayload) -> TribeResult<TxSignature> {
let kp = match &self.relayer_keypair {
Some(k) => Arc::clone(k),
None => {
tracing::warn!(
challenge = %proof.proof.challenge_id,
miner = %proof.proof.miner_pubkey,
"No relayer keypair; returning stub signature"
);
return Ok(Self::stub_signature(proof));
}
};
if self.auto_register_miners {
if let Ok(None) = self.query_miner(&proof.proof.miner_pubkey).await {
tracing::info!(
miner = %proof.proof.miner_pubkey,
"Miner not on-chain; auto-registering before submit"
);
if let Err(e) = self.register_miner(&proof.proof.miner_pubkey).await {
tracing::warn!(
miner = %proof.proof.miner_pubkey,
error = %e,
"Auto-register failed (may already exist); continuing with submit"
);
}
}
}
tracing::info!(
challenge = %proof.proof.challenge_id,
miner = %proof.proof.miner_pubkey,
program = %self.program_id,
relayer = %kp.pubkey(),
"Submitting proof to Solana"
);
let rpc_url = self.rpc_url.clone();
let proof_clone = proof.clone();
let rpc_url_slot = rpc_url.clone();
let challenge_slot = tokio::task::spawn_blocking(move || {
let client = RpcClient::new(&rpc_url_slot);
client.get_slot().map_err(Box::new)
})
.await
.map_err(|e| TribeError::ChainBridgeError(format!("spawn_blocking join: {e}")))?
.map_err(|e| TribeError::ChainBridgeError(format!("get_slot: {e}")))?;
tracing::debug!(challenge_slot, "Fetched current Solana slot");
let ix = self.build_submit_proof_ix(&proof_clone, challenge_slot)?;
let sig = tokio::task::spawn_blocking(move || -> TribeResult<String> {
let client = RpcClient::new(&rpc_url);
let blockhash = client
.get_latest_blockhash()
.map_err(|e| TribeError::ChainBridgeError(format!("get_latest_blockhash: {e}")))?;
let tx =
Transaction::new_signed_with_payer(&[ix], Some(&kp.pubkey()), &[&kp], blockhash);
let signature = client
.send_and_confirm_transaction(&tx)
.map_err(|e| TribeError::ChainBridgeError(format!("send_and_confirm: {e}")))?;
Ok(signature.to_string())
})
.await
.map_err(|e| TribeError::ChainBridgeError(format!("spawn_blocking join: {e}")))??;
tracing::info!(tx_signature = %sig, "Proof submitted to Solana successfully");
Ok(TxSignature(sig))
}
async fn query_miner(&self, pubkey: &str) -> TribeResult<Option<MinerAccount>> {
let miner_pubkey = Pubkey::from_str(pubkey)
.map_err(|e| TribeError::ChainBridgeError(format!("invalid miner pubkey: {e}")))?;
let (miner_pda, _) =
Pubkey::find_program_address(&[b"miner", miner_pubkey.as_ref()], &self.program_id);
let rpc_url = self.rpc_url.clone();
let _program_id = self.program_id;
let result = tokio::task::spawn_blocking(move || -> TribeResult<Option<MinerAccount>> {
let client = RpcClient::new(&rpc_url);
match client.get_account(&miner_pda) {
Ok(account) => {
let data = account.data;
if data.len() < 8 {
return Ok(None);
}
let payload = &data[8..];
match OnChainMinerAccount::try_from_slice(payload) {
Ok(on_chain) => Ok(Some(MinerAccount {
pubkey: on_chain.owner.to_string(),
total_proofs: on_chain.total_proofs,
total_rewards: on_chain.total_rewards,
pending_rewards: on_chain.pending_rewards,
reputation_score: on_chain.reputation_score,
last_proof_slot: on_chain.last_proof_slot,
})),
Err(e) => {
tracing::debug!(error = %e, "Failed to deserialize miner account");
Ok(None)
}
}
}
Err(_) => Ok(None),
}
})
.await
.map_err(|e| TribeError::ChainBridgeError(format!("spawn_blocking join: {e}")))??;
tracing::debug!(
pubkey,
found = result.is_some(),
"Querying miner account on-chain"
);
Ok(result)
}
async fn register_miner(&self, miner_pubkey: &str) -> TribeResult<TxSignature> {
let kp = match &self.relayer_keypair {
Some(k) => Arc::clone(k),
None => {
return Err(TribeError::ChainBridgeError(
"No relayer keypair; cannot register miner".into(),
));
}
};
let miner_pubkey = Pubkey::from_str(miner_pubkey)
.map_err(|e| TribeError::ChainBridgeError(format!("invalid miner pubkey: {e}")))?;
let ix = self.build_register_miner_ix(&miner_pubkey)?;
let rpc_url = self.rpc_url.clone();
let sig = tokio::task::spawn_blocking(move || -> TribeResult<String> {
let client = RpcClient::new(&rpc_url);
let blockhash = client
.get_latest_blockhash()
.map_err(|e| TribeError::ChainBridgeError(format!("get_latest_blockhash: {e}")))?;
let tx =
Transaction::new_signed_with_payer(&[ix], Some(&kp.pubkey()), &[&kp], blockhash);
match client.send_and_confirm_transaction(&tx) {
Ok(signature) => {
tracing::info!(
miner = %miner_pubkey,
tx_signature = %signature,
"Miner registered on-chain"
);
Ok(signature.to_string())
}
Err(e) => {
let err_str = e.to_string();
if err_str.contains("already in use") || err_str.contains("already exists") {
tracing::debug!(
miner = %miner_pubkey,
"Miner account already exists (idempotent)"
);
Ok(format!("already_registered_{}", miner_pubkey))
} else {
Err(TribeError::ChainBridgeError(format!(
"register_miner send_and_confirm: {e}"
)))
}
}
}
})
.await
.map_err(|e| TribeError::ChainBridgeError(format!("spawn_blocking join: {e}")))??;
Ok(TxSignature(sig))
}
async fn get_current_difficulty(&self) -> TribeResult<u64> {
Ok(2)
}
async fn request_swap(
&self,
from_token: Token,
to_token: Token,
amount: u64,
) -> TribeResult<TxSignature> {
tracing::info!(
?from_token,
?to_token,
amount,
"Swap request (CPI to tribewarez-swap)"
);
Ok(TxSignature("sim_swap_placeholder".into()))
}
}
pub struct EvmBridge {
pub rpc_url: String,
pub contract_address: String,
}
#[async_trait]
impl ChainBridge for EvmBridge {
async fn submit_proof(&self, _proof: &ProofPayload) -> TribeResult<TxSignature> {
todo!("EVM proof submission not yet implemented")
}
async fn query_miner(&self, _pubkey: &str) -> TribeResult<Option<MinerAccount>> {
todo!("EVM miner query not yet implemented")
}
async fn register_miner(&self, _miner_pubkey: &str) -> TribeResult<TxSignature> {
todo!("EVM miner registration not yet implemented")
}
async fn get_current_difficulty(&self) -> TribeResult<u64> {
todo!("EVM difficulty query not yet implemented")
}
async fn request_swap(
&self,
_from: Token,
_to: Token,
_amount: u64,
) -> TribeResult<TxSignature> {
todo!("EVM swap not yet implemented")
}
}
pub struct CrossChainBridge {
pub solana_rpc_url: String,
pub evm_rpc_url: String,
}
#[async_trait]
impl ChainBridge for CrossChainBridge {
async fn submit_proof(&self, _proof: &ProofPayload) -> TribeResult<TxSignature> {
todo!("Cross-chain proof submission not yet implemented")
}
async fn query_miner(&self, _pubkey: &str) -> TribeResult<Option<MinerAccount>> {
todo!("Cross-chain miner query not yet implemented")
}
async fn register_miner(&self, _miner_pubkey: &str) -> TribeResult<TxSignature> {
todo!("Cross-chain miner registration not yet implemented")
}
async fn get_current_difficulty(&self) -> TribeResult<u64> {
todo!("Cross-chain difficulty query not yet implemented")
}
async fn request_swap(
&self,
_from: Token,
_to: Token,
_amount: u64,
) -> TribeResult<TxSignature> {
todo!("Cross-chain atomic swap not yet implemented")
}
}