synor/apps/synord/src/services/miner.rs
Gulshan Yadav 48949ebb3f Initial commit: Synor blockchain monorepo
A complete blockchain implementation featuring:
- synord: Full node with GHOSTDAG consensus
- explorer-web: Modern React blockchain explorer with 3D DAG visualization
- CLI wallet and tools
- Smart contract SDK and example contracts (DEX, NFT, token)
- WASM crypto library for browser/mobile
2026-01-08 05:22:17 +05:30

550 lines
18 KiB
Rust

//! 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<CrateMiningStats> 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<ConsensusService>,
/// Mempool reference.
mempool: Arc<MempoolService>,
/// Coinbase address.
coinbase_address: Option<Address>,
/// Raw coinbase address string for display.
coinbase_address_str: Option<String>,
/// Number of threads.
threads: usize,
/// Extra data for coinbase.
extra_data: String,
/// Network type.
network: Network,
/// The underlying block miner.
miner: Arc<BlockMiner>,
/// Command sender for the miner.
cmd_tx: mpsc::Sender<MinerCommand>,
/// Is mining active.
is_mining: AtomicBool,
/// Total hashes counter.
total_hashes: AtomicU64,
/// Blocks found counter.
blocks_found: AtomicU64,
/// Mining stats (local tracking).
stats: RwLock<MiningStats>,
/// Is running.
running: RwLock<bool>,
/// Current template ID.
template_id: AtomicU64,
/// Shutdown receiver.
shutdown_rx: RwLock<Option<broadcast::Receiver<()>>>,
/// 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<ConsensusService>,
mempool: Arc<MempoolService>,
config: &NodeConfig,
shutdown_rx: broadcast::Receiver<()>,
) -> anyhow::Result<Self> {
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::<Address>().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<Self>) -> 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<MiningBlockTemplate> {
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<MiningBlockTemplate> {
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<Vec<u8>> {
// 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<u8>) -> 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
}