//! Miner service. use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; use tokio::sync::{broadcast, mpsc, RwLock}; use tracing::{debug, error, info, warn}; use synor_mining::{ BlockMiner, BlockTemplate as MiningBlockTemplate, BlockTemplateBuilder, CoinbaseBuilder, MinerCommand, MinerConfig, MinerEvent, MiningResult, MiningStats as CrateMiningStats, TemplateTransaction, }; use synor_types::{Address, Hash256, Network}; use crate::config::NodeConfig; use crate::services::{ConsensusService, MempoolService}; /// Mining statistics for the node. #[derive(Clone, Debug, Default)] pub struct MiningStats { /// Total hashes computed. pub hashes: u64, /// Blocks found. pub blocks_found: u64, /// Current hashrate (H/s). pub hashrate: f64, /// Last block found timestamp. pub last_block_time: u64, /// Mining start time. pub start_time: u64, /// Is currently mining. pub is_mining: bool, /// Formatted hashrate string. pub formatted_hashrate: String, } impl From for MiningStats { fn from(stats: CrateMiningStats) -> Self { MiningStats { hashes: stats.total_hashes, blocks_found: stats.blocks_found, hashrate: stats.hashrate, last_block_time: stats.last_block_time, start_time: 0, is_mining: false, // Set by service formatted_hashrate: stats.formatted_hashrate(), } } } /// Miner service manages block mining using synor-mining crate. pub struct MinerService { /// Consensus reference. consensus: Arc, /// Mempool reference. mempool: Arc, /// Coinbase address. coinbase_address: Option
, /// Raw coinbase address string for display. coinbase_address_str: Option, /// Number of threads. threads: usize, /// Extra data for coinbase. extra_data: String, /// Network type. network: Network, /// The underlying block miner. miner: Arc, /// Command sender for the miner. cmd_tx: mpsc::Sender, /// Is mining active. is_mining: AtomicBool, /// Total hashes counter. total_hashes: AtomicU64, /// Blocks found counter. blocks_found: AtomicU64, /// Mining stats (local tracking). stats: RwLock, /// Is running. running: RwLock, /// Current template ID. template_id: AtomicU64, /// Shutdown receiver. shutdown_rx: RwLock>>, /// Block found channel (hash of found blocks). block_found_tx: broadcast::Sender<[u8; 32]>, } impl MinerService { /// Creates a new miner service. pub async fn new( consensus: Arc, mempool: Arc, config: &NodeConfig, shutdown_rx: broadcast::Receiver<()>, ) -> anyhow::Result { let (block_found_tx, _) = broadcast::channel(100); let threads = if config.mining.threads == 0 { num_cpus::get() } else { config.mining.threads }; // Parse coinbase address if provided let coinbase_address = config.mining.coinbase_address.as_ref().and_then(|addr_str| { addr_str.parse::
().ok() }); // Determine network from config let network = match config.network.as_str() { "testnet" => Network::Testnet, "devnet" => Network::Devnet, _ => Network::Mainnet, }; // Create miner config let miner_address = coinbase_address.clone().unwrap_or_else(|| { // Default placeholder address (won't mine without real address) Address::from_ed25519_pubkey(network, &[0; 32]) }); let miner_config = MinerConfig::solo(miner_address, threads); let miner = Arc::new(BlockMiner::new(miner_config)); let cmd_tx = miner.command_sender(); Ok(MinerService { consensus, mempool, coinbase_address, coinbase_address_str: config.mining.coinbase_address.clone(), threads, extra_data: config.mining.extra_data.clone(), network, miner, cmd_tx, is_mining: AtomicBool::new(false), total_hashes: AtomicU64::new(0), blocks_found: AtomicU64::new(0), stats: RwLock::new(MiningStats::default()), running: RwLock::new(false), template_id: AtomicU64::new(0), shutdown_rx: RwLock::new(Some(shutdown_rx)), block_found_tx, }) } /// Starts the miner service. pub async fn start(self: &Arc) -> anyhow::Result<()> { if self.coinbase_address.is_none() { warn!("Mining enabled but no coinbase address set"); return Ok(()); } info!( threads = self.threads, address = ?self.coinbase_address_str, "Starting miner" ); *self.running.write().await = true; self.is_mining.store(true, Ordering::SeqCst); // Update stats { let mut stats = self.stats.write().await; stats.is_mining = true; stats.start_time = current_timestamp(); } // Subscribe to miner events and spawn event handler let mut event_rx = self.miner.subscribe(); let service = Arc::clone(self); tokio::spawn(async move { while let Ok(event) = event_rx.recv().await { match event { MinerEvent::BlockFound(result) => { info!( nonce = result.nonce, hashes = result.hashes, solve_time_ms = result.solve_time_ms, "Block found!" ); // Update stats service.blocks_found.fetch_add(1, Ordering::SeqCst); { let mut stats = service.stats.write().await; stats.blocks_found += 1; stats.last_block_time = current_timestamp(); } // Notify listeners let _ = service.block_found_tx.send(*result.pow_hash.as_bytes()); // Build and submit the block if let Err(e) = service.submit_found_block(&result).await { error!("Failed to submit found block: {}", e); } // Get new template and continue mining if service.is_mining.load(Ordering::SeqCst) { if let Err(e) = service.update_template().await { warn!("Failed to get new template after block found: {}", e); } } } MinerEvent::StatsUpdate(crate_stats) => { let mut stats = service.stats.write().await; stats.hashes = crate_stats.total_hashes; stats.hashrate = crate_stats.hashrate; stats.formatted_hashrate = crate_stats.formatted_hashrate(); } MinerEvent::Started => { info!("Mining started"); } MinerEvent::Stopped => { info!("Mining stopped"); } MinerEvent::Paused => { info!("Mining paused"); } MinerEvent::Resumed => { info!("Mining resumed"); } MinerEvent::Error(err) => { error!("Mining error: {}", err); } } } }); // Run the miner's async loop in background let miner = Arc::clone(&self.miner); tokio::spawn(async move { miner.run().await; }); // Get initial template and start mining self.update_template().await?; Ok(()) } /// Updates the mining template. async fn update_template(&self) -> anyhow::Result<()> { let template = self.build_template().await?; let _ = self.cmd_tx.send(MinerCommand::NewTemplate(Arc::new(template))).await; Ok(()) } /// Stops the miner service. pub async fn stop(&self) -> anyhow::Result<()> { info!("Stopping miner"); self.is_mining.store(false, Ordering::SeqCst); *self.running.write().await = false; // Send stop command to miner let _ = self.cmd_tx.send(MinerCommand::Stop).await; { let mut stats = self.stats.write().await; stats.is_mining = false; } Ok(()) } /// Checks if mining. pub fn is_mining(&self) -> bool { self.is_mining.load(Ordering::SeqCst) && self.miner.is_mining() } /// Gets mining stats. pub async fn stats(&self) -> MiningStats { // Merge local stats with miner stats let crate_stats = self.miner.stats(); let mut stats = self.stats.read().await.clone(); stats.hashes = crate_stats.total_hashes; stats.hashrate = crate_stats.hashrate; stats.formatted_hashrate = crate_stats.formatted_hashrate(); stats } /// Sets coinbase address. pub async fn set_coinbase_address(&self, address: String) -> anyhow::Result<()> { let parsed: Address = address.parse() .map_err(|e| anyhow::anyhow!("Invalid address: {}", e))?; // Update miner config let new_config = MinerConfig::solo(parsed, self.threads); let _ = self.cmd_tx.send(MinerCommand::UpdateConfig(new_config)).await; info!(address = %address, "Updated coinbase address"); Ok(()) } /// Builds a block template for mining. async fn build_template(&self) -> anyhow::Result { let coinbase_address = self.coinbase_address.clone() .ok_or_else(|| anyhow::anyhow!("No coinbase address set"))?; // Get transactions from mempool let max_mass = 500_000u64; // TODO: From config let mempool_txs = self.mempool.select_for_block(max_mass).await; // Get current DAG tips let tips = self.consensus.tips().await; let blue_score = self.consensus.blue_score().await; let bits = self.consensus.current_difficulty().await; // Build coinbase let block_reward = self.get_block_reward().await; let fees: u64 = mempool_txs.iter().map(|tx| tx.fee).sum(); let coinbase = CoinbaseBuilder::new(coinbase_address, blue_score) .extra_data(self.extra_data.as_bytes().to_vec()) .reward(block_reward) .fees(fees) .build(); // Build template let template_id = self.template_id.fetch_add(1, Ordering::SeqCst); let mut builder = BlockTemplateBuilder::new() .version(1) .timestamp(current_timestamp()) .bits(bits) .blue_score(blue_score) .coinbase(coinbase) .reward(block_reward); // Add parents for tip in tips { builder = builder.add_parent(Hash256::from_bytes(tip)); } // Add transactions for tx in mempool_txs { let template_tx = TemplateTransaction { txid: Hash256::from_bytes(tx.hash), data: tx.data, fee: tx.fee, mass: tx.mass, }; builder = builder.add_transaction(template_tx); } let template = builder.build(template_id) .map_err(|e| anyhow::anyhow!("Failed to build template: {}", e))?; debug!( template_id = template_id, parents = template.parent_hashes.len(), transactions = template.transactions.len(), reward = template.block_reward, fees = template.total_fees, "Built mining template" ); Ok(template) } /// Gets current block template (for RPC). pub async fn get_template(&self) -> anyhow::Result { self.build_template().await } /// Gets the block reward for current height. async fn get_block_reward(&self) -> u64 { // TODO: Get from emission schedule based on blue score let blue_score = self.consensus.blue_score().await; // Simple emission schedule: halving every 210,000 blocks // Starting reward: 500 SYNOR = 500_00000000 sompi let halvings = blue_score / 210_000; let initial_reward = 500_00000000u64; if halvings >= 64 { 0 // No more rewards after ~64 halvings } else { initial_reward >> halvings } } /// Calculates coinbase value (block reward + fees). pub async fn calculate_coinbase_value(&self) -> u64 { let block_reward = self.get_block_reward().await; let mempool_stats = self.mempool.stats().await; block_reward + mempool_stats.total_fees } /// Submits a found block to consensus. async fn submit_found_block(&self, result: &MiningResult) -> anyhow::Result<()> { info!( template_id = result.template_id, nonce = result.nonce, "Submitting found block" ); // Get the template that was mined let template = self.miner.current_template() .ok_or_else(|| anyhow::anyhow!("No current template"))?; // Build full block from template and mining result let block_bytes = self.build_block_bytes(&template, result)?; // Validate and process let validation = self.consensus.validate_block(&block_bytes).await; match validation { crate::services::consensus::BlockValidation::Valid => { self.consensus.process_block_bytes(&block_bytes).await?; info!("Block submitted successfully"); } crate::services::consensus::BlockValidation::Invalid { reason } => { warn!(reason = %reason, "Mined block was invalid"); return Err(anyhow::anyhow!("Invalid block: {}", reason)); } _ => { warn!("Unexpected block validation result"); } } Ok(()) } /// Builds block bytes from template and mining result. fn build_block_bytes( &self, template: &MiningBlockTemplate, result: &MiningResult, ) -> anyhow::Result> { // Build complete block: // - Header with nonce // - Transactions let mut block = Vec::new(); // Header (template header data + nonce) let mut header = template.header_for_mining(); header.extend_from_slice(&result.nonce.to_le_bytes()); block.extend_from_slice(&header); // Transaction count (varint encoding for simplicity) let tx_count = template.transactions.len() as u64; block.extend_from_slice(&tx_count.to_le_bytes()); // Transactions for tx in &template.transactions { // Length prefix let tx_len = tx.data.len() as u32; block.extend_from_slice(&tx_len.to_le_bytes()); block.extend_from_slice(&tx.data); } Ok(block) } /// Submits a mined block (for external submission via RPC). pub async fn submit_block(&self, block: Vec) -> anyhow::Result<()> { info!("Submitting externally mined block"); let validation = self.consensus.validate_block(&block).await; match validation { crate::services::consensus::BlockValidation::Valid => { self.consensus.process_block_bytes(&block).await?; // Update stats self.blocks_found.fetch_add(1, Ordering::SeqCst); { let mut stats = self.stats.write().await; stats.blocks_found += 1; stats.last_block_time = current_timestamp(); } // Get hash from block header for notification let hash = if block.len() >= 32 { let mut h = [0u8; 32]; h.copy_from_slice(&blake3::hash(&block[..96.min(block.len())]).as_bytes()[..32]); h } else { [0u8; 32] }; let _ = self.block_found_tx.send(hash); info!("External block submitted successfully"); } crate::services::consensus::BlockValidation::Invalid { reason } => { warn!(reason = %reason, "Submitted block was invalid"); return Err(anyhow::anyhow!("Invalid block: {}", reason)); } _ => { warn!("Unexpected block validation result"); } } Ok(()) } /// Subscribes to found blocks. pub fn subscribe_blocks(&self) -> broadcast::Receiver<[u8; 32]> { self.block_found_tx.subscribe() } /// Gets current hashrate. pub fn hashrate(&self) -> f64 { self.miner.hashrate() } /// Gets hash count. pub fn hash_count(&self) -> u64 { self.miner.hash_count() } /// Pauses mining. pub async fn pause(&self) -> anyhow::Result<()> { let _ = self.cmd_tx.send(MinerCommand::Pause).await; Ok(()) } /// Resumes mining. pub async fn resume(&self) -> anyhow::Result<()> { let _ = self.cmd_tx.send(MinerCommand::Resume).await; Ok(()) } } fn current_timestamp() -> u64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() as u64 }