Skip to main content

abtc_adapters/mining/
mod.rs

1//! Mining Provider Implementation
2//!
3//! Provides a block template provider that:
4//! - Selects transactions from the mempool by fee rate
5//! - Computes the correct block subsidy following the halving schedule
6//! - Builds a complete block template ready for mining
7
8use abtc_domain::consensus::ConsensusParams;
9use abtc_domain::primitives::{Amount, Block, BlockHash, BlockHeader, Hash256, Transaction, TxOut};
10use abtc_domain::script::Script;
11use abtc_ports::{BlockTemplate, BlockTemplateProvider, MempoolPort};
12use async_trait::async_trait;
13use std::sync::Arc;
14use tokio::sync::RwLock;
15
16/// Reserved weight for the coinbase transaction
17const COINBASE_RESERVED_WEIGHT: u32 = 4_000;
18
19/// Maximum block weight (4 million weight units)
20const MAX_BLOCK_WEIGHT: u32 = 4_000_000;
21
22/// Mining block template provider with mempool-aware transaction selection
23/// and correct halving-based subsidy calculation.
24pub struct SimpleMiner {
25    current_height: Arc<RwLock<u32>>,
26    best_block_hash: Arc<RwLock<BlockHash>>,
27    mempool: Option<Arc<dyn MempoolPort>>,
28}
29
30impl SimpleMiner {
31    /// Create a new miner without mempool access (empty blocks only)
32    pub fn new() -> Self {
33        SimpleMiner {
34            current_height: Arc::new(RwLock::new(0)),
35            best_block_hash: Arc::new(RwLock::new(BlockHash::zero())),
36            mempool: None,
37        }
38    }
39
40    /// Create a new miner with mempool access for transaction selection
41    pub fn with_mempool(mempool: Arc<dyn MempoolPort>) -> Self {
42        SimpleMiner {
43            current_height: Arc::new(RwLock::new(0)),
44            best_block_hash: Arc::new(RwLock::new(BlockHash::zero())),
45            mempool: Some(mempool),
46        }
47    }
48
49    /// Set the current block height
50    pub async fn set_height(&self, height: u32) {
51        let mut h = self.current_height.write().await;
52        *h = height;
53    }
54
55    /// Set the best block hash
56    pub async fn set_best_block_hash(&self, hash: BlockHash) {
57        let mut b = self.best_block_hash.write().await;
58        *b = hash;
59    }
60
61    /// Calculate block subsidy for a given height following the halving schedule.
62    ///
63    /// Bitcoin starts at 50 BTC per block and halves every 210,000 blocks.
64    /// After 64 halvings, the subsidy drops to 0.
65    fn get_block_subsidy(height: u32, params: &ConsensusParams) -> Amount {
66        let halving_interval = params.subsidy_halving_interval;
67        if halving_interval == 0 {
68            return Amount::from_sat(5_000_000_000);
69        }
70
71        let halvings = height / halving_interval;
72        if halvings >= 64 {
73            return Amount::from_sat(0);
74        }
75
76        // Initial subsidy: 50 BTC = 5,000,000,000 satoshis
77        let initial_subsidy: i64 = 50 * 100_000_000;
78        let subsidy = initial_subsidy >> halvings;
79        Amount::from_sat(subsidy)
80    }
81
82    /// Select transactions from the mempool ordered by fee rate,
83    /// fitting within the block weight limit.
84    async fn select_mempool_transactions(&self) -> (Vec<Transaction>, Vec<Amount>) {
85        let mempool = match &self.mempool {
86            Some(m) => m,
87            None => return (Vec::new(), Vec::new()),
88        };
89
90        let entries = match mempool.get_all_transactions().await {
91            Ok(e) => e,
92            Err(_) => return (Vec::new(), Vec::new()),
93        };
94
95        if entries.is_empty() {
96            return (Vec::new(), Vec::new());
97        }
98
99        // Sort by fee rate descending (greedy selection)
100        let mut sorted = entries;
101        sorted.sort_by(|a, b| {
102            let rate_a = a.fee.as_sat() as f64 / a.size.max(1) as f64;
103            let rate_b = b.fee.as_sat() as f64 / b.size.max(1) as f64;
104            rate_b
105                .partial_cmp(&rate_a)
106                .unwrap_or(std::cmp::Ordering::Equal)
107        });
108
109        let mut selected_txs = Vec::new();
110        let mut selected_fees = Vec::new();
111        let mut total_weight: u32 = COINBASE_RESERVED_WEIGHT;
112
113        for entry in sorted {
114            let tx_weight = (entry.size as u32) * 4;
115            if total_weight + tx_weight > MAX_BLOCK_WEIGHT {
116                continue;
117            }
118            total_weight += tx_weight;
119            selected_fees.push(entry.fee);
120            selected_txs.push(entry.tx);
121        }
122
123        (selected_txs, selected_fees)
124    }
125}
126
127impl Default for SimpleMiner {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133#[async_trait]
134impl BlockTemplateProvider for SimpleMiner {
135    async fn create_block_template(
136        &self,
137        coinbase_script: &Script,
138        params: &ConsensusParams,
139    ) -> Result<BlockTemplate, Box<dyn std::error::Error + Send + Sync>> {
140        let height = *self.current_height.read().await;
141        let prev_hash = *self.best_block_hash.read().await;
142
143        // Select transactions from mempool
144        let (mempool_txs, mempool_fees) = self.select_mempool_transactions().await;
145
146        // Block subsidy with proper halving
147        let subsidy = Self::get_block_subsidy(height, params);
148        let total_fees: i64 = mempool_fees.iter().map(|f| f.as_sat()).sum();
149        let coinbase_reward = Amount::from_sat(subsidy.as_sat() + total_fees);
150
151        // Create coinbase transaction
152        let coinbase_tx = Transaction::coinbase(
153            height,
154            coinbase_script.clone(),
155            vec![TxOut::new(coinbase_reward, coinbase_script.clone())],
156        );
157
158        // Assemble transactions: coinbase first, then mempool
159        let mut transactions = vec![coinbase_tx];
160        transactions.extend(mempool_txs);
161
162        // Compute merkle root
163        let now = std::time::SystemTime::now()
164            .duration_since(std::time::UNIX_EPOCH)
165            .unwrap_or_default()
166            .as_secs() as u32;
167
168        let difficulty_bits = params.pow_limit_bits;
169
170        let temp_block = Block::new(
171            BlockHeader::new(
172                0x20000000,
173                prev_hash,
174                Hash256::zero(),
175                now,
176                difficulty_bits,
177                0,
178            ),
179            transactions.clone(),
180        );
181        let merkle_root = temp_block.compute_merkle_root();
182
183        // Build final block with correct merkle root
184        let header = BlockHeader::new(0x20000000, prev_hash, merkle_root, now, difficulty_bits, 0);
185        let block = Block::new(header, transactions);
186
187        let mut fees = vec![Amount::from_sat(0)]; // Coinbase
188        fees.extend(mempool_fees);
189
190        let sigops = vec![0u64; fees.len()];
191
192        let template = BlockTemplate {
193            block,
194            fees,
195            sigops,
196            target: difficulty_bits,
197            height,
198        };
199
200        tracing::debug!(
201            "Created block template for height {} ({} txs, subsidy {} sat, fees {} sat)",
202            height,
203            template.block.transactions.len(),
204            subsidy.as_sat(),
205            total_fees
206        );
207
208        Ok(template)
209    }
210
211    async fn get_block_height(&self) -> Result<u32, Box<dyn std::error::Error + Send + Sync>> {
212        let h = self.current_height.read().await;
213        Ok(*h)
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[tokio::test]
222    async fn test_simple_miner_creation() {
223        let miner = SimpleMiner::new();
224        assert_eq!(miner.get_block_height().await.unwrap(), 0);
225    }
226
227    #[tokio::test]
228    async fn test_create_block_template() {
229        let miner = SimpleMiner::new();
230        miner.set_height(1).await;
231        miner
232            .set_best_block_hash(BlockHash::genesis_mainnet())
233            .await;
234
235        let params = ConsensusParams::mainnet();
236        let template = miner
237            .create_block_template(&Script::new(), &params)
238            .await
239            .unwrap();
240
241        assert_eq!(template.height, 1);
242        assert!(!template.block.transactions.is_empty());
243        // Should have 50 BTC reward at height 1
244        let coinbase_value = template.block.transactions[0].total_output_value();
245        assert_eq!(coinbase_value.as_sat(), 5_000_000_000);
246    }
247
248    #[test]
249    fn test_block_subsidy_initial() {
250        let params = ConsensusParams::mainnet();
251        assert_eq!(
252            SimpleMiner::get_block_subsidy(0, &params).as_sat(),
253            5_000_000_000
254        );
255    }
256
257    #[test]
258    fn test_block_subsidy_first_halving() {
259        let params = ConsensusParams::mainnet();
260        assert_eq!(
261            SimpleMiner::get_block_subsidy(210_000, &params).as_sat(),
262            2_500_000_000
263        );
264    }
265
266    #[test]
267    fn test_block_subsidy_second_halving() {
268        let params = ConsensusParams::mainnet();
269        assert_eq!(
270            SimpleMiner::get_block_subsidy(420_000, &params).as_sat(),
271            1_250_000_000
272        );
273    }
274
275    #[test]
276    fn test_block_subsidy_exhausted() {
277        let params = ConsensusParams::mainnet();
278        assert_eq!(
279            SimpleMiner::get_block_subsidy(210_000 * 64, &params).as_sat(),
280            0
281        );
282    }
283}