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
550 lines
18 KiB
Rust
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
|
|
}
|