Fix all Rust clippy warnings that were causing CI failures when built with RUSTFLAGS=-Dwarnings. Changes include: - Replace derivable_impls with derive macros for BlockBody, Network, etc. - Use div_ceil() instead of manual implementation - Fix should_implement_trait by renaming from_str to parse - Add type aliases for type_complexity warnings - Use or_default(), is_some_and(), is_multiple_of() where appropriate - Remove needless borrows and redundant closures - Fix manual_strip with strip_prefix() - Add allow attributes for intentional patterns (too_many_arguments, needless_range_loop in cryptographic code, assertions_on_constants) - Remove unused imports, mut bindings, and dead code in tests
515 lines
14 KiB
Rust
515 lines
14 KiB
Rust
//! 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<Hash256>,
|
|
/// 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<TemplateTransaction>,
|
|
/// 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<u8>,
|
|
/// 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<u8> {
|
|
self.header_data.clone()
|
|
}
|
|
|
|
/// Creates header with a specific extra nonce.
|
|
pub fn header_with_extra_nonce(&self, extra_nonce: u64) -> Vec<u8> {
|
|
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<u8>,
|
|
/// Script for payout.
|
|
pub script: Vec<u8>,
|
|
}
|
|
|
|
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<u8>) -> 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<u8>,
|
|
/// 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<Hash256>,
|
|
/// Selected parent.
|
|
selected_parent: Option<Hash256>,
|
|
/// Blue score.
|
|
blue_score: u64,
|
|
/// Difficulty bits.
|
|
bits: u32,
|
|
/// Timestamp.
|
|
timestamp: u64,
|
|
/// Coinbase data.
|
|
coinbase: Option<CoinbaseData>,
|
|
/// Transactions.
|
|
transactions: Vec<TemplateTransaction>,
|
|
/// 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<Hash256>) -> 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<BlockTemplate, MiningError> {
|
|
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<Vec<u8>, 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<u8>,
|
|
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<u8>) -> 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<u8> {
|
|
// 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);
|
|
}
|
|
}
|