abtc_adapters/mining/
mod.rs1use 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
16const COINBASE_RESERVED_WEIGHT: u32 = 4_000;
18
19const MAX_BLOCK_WEIGHT: u32 = 4_000_000;
21
22pub 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 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 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 pub async fn set_height(&self, height: u32) {
51 let mut h = self.current_height.write().await;
52 *h = height;
53 }
54
55 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 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 let initial_subsidy: i64 = 50 * 100_000_000;
78 let subsidy = initial_subsidy >> halvings;
79 Amount::from_sat(subsidy)
80 }
81
82 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 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 let (mempool_txs, mempool_fees) = self.select_mempool_transactions().await;
145
146 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 let coinbase_tx = Transaction::coinbase(
153 height,
154 coinbase_script.clone(),
155 vec![TxOut::new(coinbase_reward, coinbase_script.clone())],
156 );
157
158 let mut transactions = vec![coinbase_tx];
160 transactions.extend(mempool_txs);
161
162 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 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)]; 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(), ¶ms)
238 .await
239 .unwrap();
240
241 assert_eq!(template.height, 1);
242 assert!(!template.block.transactions.is_empty());
243 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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).as_sat(),
280 0
281 );
282 }
283}