//! Block template creation for mining. //! //! Templates provide miners with all the information needed to construct //! and mine a valid block. use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; use synor_types::{Address, Hash256}; use crate::{MiningError, Target}; /// A block template for mining. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct BlockTemplate { /// Template identifier (for tracking). pub id: u64, /// Version of the template format. pub version: u32, /// Parent block hashes (DAG structure). pub parent_hashes: Vec, /// Selected parent (for GHOSTDAG). pub selected_parent: Hash256, /// GHOSTDAG blue score. pub blue_score: u64, /// Bits representation of target. pub bits: u32, /// Target for mining. pub target: [u8; 32], /// Block timestamp. pub timestamp: u64, /// Coinbase transaction data. pub coinbase_data: CoinbaseData, /// Transactions to include. pub transactions: Vec, /// Total fees from transactions. pub total_fees: u64, /// Block reward (subsidy). pub block_reward: u64, /// Merkle root of transactions. pub tx_merkle_root: Hash256, /// Accepted ID merkle root. pub accepted_id_merkle_root: Hash256, /// UTXO commitment. pub utxo_commitment: Hash256, /// Header bytes for mining (without nonce). pub header_data: Vec, /// Extra nonce space for pool mining. pub extra_nonce_range: (u64, u64), } impl BlockTemplate { /// Creates the header bytes for hashing. pub fn header_for_mining(&self) -> Vec { self.header_data.clone() } /// Creates header with a specific extra nonce. pub fn header_with_extra_nonce(&self, extra_nonce: u64) -> Vec { let mut header = self.header_data.clone(); // Insert extra nonce at designated position (after merkle root) if header.len() >= 40 { let nonce_bytes = extra_nonce.to_le_bytes(); header[32..40].copy_from_slice(&nonce_bytes); } header } /// Gets the target. pub fn get_target(&self) -> Target { Target::from_bytes(self.target) } /// Validates the template is well-formed. pub fn validate(&self) -> Result<(), MiningError> { if self.parent_hashes.is_empty() { return Err(MiningError::InvalidTemplate("No parent hashes".into())); } if !self.parent_hashes.contains(&self.selected_parent) { return Err(MiningError::InvalidTemplate( "Selected parent not in parent set".into(), )); } if self.header_data.is_empty() { return Err(MiningError::InvalidTemplate("Empty header data".into())); } Ok(()) } /// Returns expected total payout (reward + fees). pub fn total_payout(&self) -> u64 { self.block_reward + self.total_fees } } /// Coinbase transaction data. #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct CoinbaseData { /// Miner address to receive rewards. pub miner_address: Address, /// Block height for coinbase maturity. pub block_height: u64, /// Extra data (pool name, etc.). pub extra_data: Vec, /// Script for payout. pub script: Vec, } impl CoinbaseData { /// Creates new coinbase data. pub fn new(miner_address: Address, block_height: u64) -> Self { CoinbaseData { miner_address, block_height, extra_data: Vec::new(), script: Vec::new(), } } /// Adds extra data (e.g., pool name). pub fn with_extra_data(mut self, data: Vec) -> Self { self.extra_data = data; self } } /// Transaction in a block template. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct TemplateTransaction { /// Transaction ID. pub txid: Hash256, /// Serialized transaction data. pub data: Vec, /// Fee paid by this transaction. pub fee: u64, /// Transaction mass (weight). pub mass: u64, } /// Builder for block templates. pub struct BlockTemplateBuilder { /// Template version. version: u32, /// Parent block hashes. parents: Vec, /// Selected parent. selected_parent: Option, /// Blue score. blue_score: u64, /// Difficulty bits. bits: u32, /// Timestamp. timestamp: u64, /// Coinbase data. coinbase: Option, /// Transactions. transactions: Vec, /// Block reward. reward: u64, } impl BlockTemplateBuilder { /// Creates a new template builder. pub fn new() -> Self { BlockTemplateBuilder { version: 1, parents: Vec::new(), selected_parent: None, blue_score: 0, bits: 0x1d00ffff, timestamp: 0, coinbase: None, transactions: Vec::new(), reward: 0, } } /// Sets the template version. pub fn version(mut self, version: u32) -> Self { self.version = version; self } /// Adds a parent hash. pub fn add_parent(mut self, hash: Hash256) -> Self { self.parents.push(hash); self } /// Sets parent hashes. pub fn parents(mut self, parents: Vec) -> Self { self.parents = parents; self } /// Sets the selected parent. pub fn selected_parent(mut self, parent: Hash256) -> Self { self.selected_parent = Some(parent); self } /// Sets the blue score. pub fn blue_score(mut self, score: u64) -> Self { self.blue_score = score; self } /// Sets difficulty bits. pub fn bits(mut self, bits: u32) -> Self { self.bits = bits; self } /// Sets timestamp. pub fn timestamp(mut self, ts: u64) -> Self { self.timestamp = ts; self } /// Sets coinbase data. pub fn coinbase(mut self, coinbase: CoinbaseData) -> Self { self.coinbase = Some(coinbase); self } /// Adds a transaction. pub fn add_transaction(mut self, tx: TemplateTransaction) -> Self { self.transactions.push(tx); self } /// Sets the block reward. pub fn reward(mut self, reward: u64) -> Self { self.reward = reward; self } /// Builds the block template. pub fn build(mut self, template_id: u64) -> Result { let selected = self .selected_parent .or_else(|| self.parents.first().copied()) .ok_or_else(|| MiningError::InvalidTemplate("No parents".into()))?; // Build header data first before taking coinbase let header_data = self.build_header(&selected)?; let coinbase = self .coinbase .take() .ok_or_else(|| MiningError::InvalidTemplate("No coinbase".into()))?; // Calculate fees let total_fees: u64 = self.transactions.iter().map(|tx| tx.fee).sum(); // Calculate merkle roots (simplified) let tx_merkle_root = self.calculate_tx_merkle_root(); let accepted_id_merkle_root = Hash256::default(); let utxo_commitment = Hash256::default(); Ok(BlockTemplate { id: template_id, version: self.version, parent_hashes: self.parents, selected_parent: selected, blue_score: self.blue_score, bits: self.bits, target: Target::from_bits(self.bits).0, timestamp: self.timestamp, coinbase_data: coinbase, transactions: self.transactions, total_fees, block_reward: self.reward, tx_merkle_root, accepted_id_merkle_root, utxo_commitment, header_data, extra_nonce_range: (0, u64::MAX), }) } fn build_header(&self, selected_parent: &Hash256) -> Result, MiningError> { // Block header format: // - Version: 4 bytes // - Parents hash (merkle of parents): 32 bytes // - Extra nonce space: 8 bytes // - TX merkle root: 32 bytes // - Timestamp: 8 bytes // - Bits: 4 bytes // - Blue score: 8 bytes // Total: 96 bytes (nonce added during mining) let mut header = Vec::with_capacity(96); // Version header.extend_from_slice(&self.version.to_le_bytes()); // Parents hash (simplified: hash of selected parent) header.extend_from_slice(selected_parent.as_bytes()); // Extra nonce space (8 bytes, filled during mining) header.extend_from_slice(&[0u8; 8]); // TX merkle root let merkle = self.calculate_tx_merkle_root(); header.extend_from_slice(merkle.as_bytes()); // Timestamp header.extend_from_slice(&self.timestamp.to_le_bytes()); // Bits header.extend_from_slice(&self.bits.to_le_bytes()); // Blue score header.extend_from_slice(&self.blue_score.to_le_bytes()); Ok(header) } fn calculate_tx_merkle_root(&self) -> Hash256 { if self.transactions.is_empty() { return Hash256::default(); } // Collect all txids let txids: Vec<&[u8; 32]> = self .transactions .iter() .map(|tx| tx.txid.as_bytes()) .collect(); // Simple merkle tree merkle_root(&txids) } } impl Default for BlockTemplateBuilder { fn default() -> Self { Self::new() } } /// Builds a coinbase transaction. pub struct CoinbaseBuilder { miner_address: Address, block_height: u64, extra_data: Vec, block_reward: u64, fees: u64, } impl CoinbaseBuilder { /// Creates a new coinbase builder. pub fn new(miner_address: Address, block_height: u64) -> Self { CoinbaseBuilder { miner_address, block_height, extra_data: Vec::new(), block_reward: 0, fees: 0, } } /// Sets extra data (pool name, etc.). pub fn extra_data(mut self, data: Vec) -> Self { self.extra_data = data; self } /// Sets the block reward. pub fn reward(mut self, reward: u64) -> Self { self.block_reward = reward; self } /// Sets the total fees. pub fn fees(mut self, fees: u64) -> Self { self.fees = fees; self } /// Builds the coinbase data. pub fn build(self) -> CoinbaseData { let script = self.build_script(); CoinbaseData { miner_address: self.miner_address, block_height: self.block_height, extra_data: self.extra_data, script, } } fn build_script(&self) -> Vec { // Simplified coinbase script // In production, this would be a proper output script let mut script = Vec::new(); // Height push (BIP34) let height_bytes = self.block_height.to_le_bytes(); let len = height_bytes.iter().rposition(|&b| b != 0).unwrap_or(0) + 1; script.push(len as u8); script.extend_from_slice(&height_bytes[..len]); // Extra data if !self.extra_data.is_empty() { script.extend_from_slice(&self.extra_data); } script } /// Total payout (reward + fees). pub fn total_payout(&self) -> u64 { self.block_reward + self.fees } } /// Computes merkle root from a list of hashes. fn merkle_root(hashes: &[&[u8; 32]]) -> Hash256 { if hashes.is_empty() { return Hash256::default(); } if hashes.len() == 1 { return Hash256::from_bytes(*hashes[0]); } let mut current: Vec<[u8; 32]> = hashes.iter().map(|h| **h).collect(); while current.len() > 1 { let mut next = Vec::with_capacity(current.len().div_ceil(2)); for chunk in current.chunks(2) { let mut data = [0u8; 64]; data[..32].copy_from_slice(&chunk[0]); if chunk.len() > 1 { data[32..].copy_from_slice(&chunk[1]); } else { data[32..].copy_from_slice(&chunk[0]); // Duplicate odd element } let hash: [u8; 32] = blake3::hash(&data).into(); next.push(hash); } current = next; } Hash256::from_bytes(current[0]) } #[cfg(test)] mod tests { use super::*; use synor_types::Network; fn test_address() -> Address { Address::from_ed25519_pubkey(Network::Mainnet, &[0x42; 32]) } fn test_hash(n: u8) -> Hash256 { let mut bytes = [0u8; 32]; bytes[0] = n; Hash256::from_bytes(bytes) } #[test] fn test_coinbase_builder() { let coinbase = CoinbaseBuilder::new(test_address(), 1000) .extra_data(b"Synor Pool".to_vec()) .reward(500_00000000) .fees(1_00000000) .build(); assert_eq!(coinbase.block_height, 1000); assert!(!coinbase.script.is_empty()); } #[test] fn test_block_template_builder() { let coinbase = CoinbaseBuilder::new(test_address(), 1) .reward(500_00000000) .build(); let template = BlockTemplateBuilder::new() .version(1) .add_parent(test_hash(1)) .timestamp(1234567890) .bits(0x1d00ffff) .blue_score(100) .coinbase(coinbase) .reward(500_00000000) .build(1) .unwrap(); assert_eq!(template.id, 1); assert!(!template.header_data.is_empty()); assert!(template.validate().is_ok()); } #[test] fn test_merkle_root() { let h1 = [1u8; 32]; let h2 = [2u8; 32]; let root1 = merkle_root(&[&h1]); assert_eq!(*root1.as_bytes(), h1); let root2 = merkle_root(&[&h1, &h2]); assert_ne!(*root2.as_bytes(), h1); assert_ne!(*root2.as_bytes(), h2); } }