1use abtc_domain::consensus::{ConsensusParams, MAX_BLOCK_WEIGHT};
17use abtc_domain::primitives::{Amount, Block, BlockHeader, Hash256, Transaction, TxOut};
18use abtc_domain::script::Script;
19use abtc_ports::{BlockTemplate, BlockTemplateProvider, ChainStateStore, MempoolPort};
20use async_trait::async_trait;
21use std::error::Error;
22use std::sync::Arc;
23
24const COINBASE_WEIGHT_RESERVE: u32 = 4_000;
27
28pub struct BlockAssembler {
34 chain_state: Arc<dyn ChainStateStore>,
35 mempool: Arc<dyn MempoolPort>,
36}
37
38impl BlockAssembler {
39 pub fn new(chain_state: Arc<dyn ChainStateStore>, mempool: Arc<dyn MempoolPort>) -> Self {
41 BlockAssembler {
42 chain_state,
43 mempool,
44 }
45 }
46
47 fn build_coinbase_script(height: u32) -> Script {
49 let mut script = Vec::new();
50
51 if height == 0 {
52 script.push(0x00); } else if height <= 16 {
54 script.push(0x50 + height as u8); } else {
56 let mut h = height;
57 let mut buf = Vec::new();
58 while h > 0 {
59 buf.push((h & 0xff) as u8);
60 h >>= 8;
61 }
62 if buf.last().is_some_and(|&b| b & 0x80 != 0) {
63 buf.push(0);
64 }
65 script.push(buf.len() as u8);
66 script.extend_from_slice(&buf);
67 }
68
69 while script.len() < 2 {
71 script.push(0x00);
72 }
73
74 Script::from_bytes(script)
75 }
76
77 fn get_block_subsidy(height: u32, params: &ConsensusParams) -> Amount {
79 let interval = params.subsidy_halving_interval;
80 if interval == 0 {
81 return Amount::from_sat(5_000_000_000);
82 }
83 let halvings = height / interval;
84 if halvings >= 64 {
85 return Amount::from_sat(0);
86 }
87 let initial: i64 = 50 * 100_000_000;
88 Amount::from_sat(initial >> halvings)
89 }
90}
91
92#[async_trait]
93impl BlockTemplateProvider for BlockAssembler {
94 async fn create_block_template(
97 &self,
98 coinbase_script: &Script,
99 params: &ConsensusParams,
100 ) -> Result<BlockTemplate, Box<dyn Error + Send + Sync>> {
101 let (prev_hash, tip_height) = self.chain_state.get_best_chain_tip().await?;
103 let new_height = tip_height + 1;
104
105 let available_weight = MAX_BLOCK_WEIGHT - COINBASE_WEIGHT_RESERVE;
108 let selected = self.mempool.get_all_transactions().await?;
109
110 let mut sorted: Vec<_> = selected.into_iter().collect();
112 sorted.sort_by(|a, b| {
113 let rate_a = a.fee.as_sat() as f64 / a.size.max(1) as f64;
114 let rate_b = b.fee.as_sat() as f64 / b.size.max(1) as f64;
115 rate_b
116 .partial_cmp(&rate_a)
117 .unwrap_or(std::cmp::Ordering::Equal)
118 });
119
120 let mut block_txs: Vec<Transaction> = Vec::new();
121 let mut total_fees = Amount::from_sat(0);
122 let mut fees_per_tx: Vec<Amount> = Vec::new();
123 let mut sigops_per_tx: Vec<u64> = Vec::new();
124 let mut total_weight: u32 = 0;
125
126 for entry in &sorted {
127 let tx_weight = (entry.size as u32) * 4; if total_weight + tx_weight > available_weight {
129 continue;
130 }
131 total_weight += tx_weight;
132 total_fees = Amount::from_sat(total_fees.as_sat() + entry.fee.as_sat());
133 fees_per_tx.push(entry.fee);
134 let sigops = (entry.tx.inputs.len() + entry.tx.outputs.len()) as u64;
136 sigops_per_tx.push(sigops);
137 block_txs.push(entry.tx.clone());
138 }
139
140 let subsidy = Self::get_block_subsidy(new_height, params);
142 let coinbase_value = Amount::from_sat(subsidy.as_sat() + total_fees.as_sat());
143
144 let coinbase_scriptsig = Self::build_coinbase_script(new_height);
145 let coinbase = Transaction::coinbase(
146 new_height,
147 coinbase_scriptsig,
148 vec![TxOut::new(coinbase_value, coinbase_script.clone())],
149 );
150
151 let mut all_txs = vec![coinbase];
153 all_txs.extend(block_txs);
154
155 let mut all_fees = vec![Amount::from_sat(0)]; all_fees.extend(fees_per_tx);
157
158 let mut all_sigops = vec![1u64]; all_sigops.extend(sigops_per_tx);
160
161 let time = std::time::SystemTime::now()
163 .duration_since(std::time::UNIX_EPOCH)
164 .unwrap_or_default()
165 .as_secs() as u32;
166
167 let bits = params.pow_limit_bits;
172
173 let temp_block = Block::new(
174 BlockHeader::new(0x20000000, prev_hash, Hash256::zero(), time, bits, 0),
175 all_txs.clone(),
176 );
177 let merkle_root = temp_block.compute_merkle_root();
178
179 let header = BlockHeader::new(0x20000000, prev_hash, merkle_root, time, bits, 0);
180 let block = Block::new(header, all_txs);
181
182 Ok(BlockTemplate {
183 block,
184 fees: all_fees,
185 sigops: all_sigops,
186 target: bits,
187 height: new_height,
188 })
189 }
190
191 async fn get_block_height(&self) -> Result<u32, Box<dyn Error + Send + Sync>> {
192 let (_, height) = self.chain_state.get_best_chain_tip().await?;
193 Ok(height + 1) }
195}
196
197#[cfg(test)]
200mod tests {
201 use super::*;
202 use abtc_domain::primitives::{BlockHash, OutPoint, TxIn, Txid};
203 use abtc_ports::{MempoolEntry, MempoolInfo, UtxoEntry};
204
205 struct MockChainState {
208 tip_hash: BlockHash,
209 tip_height: u32,
210 }
211
212 #[async_trait]
213 impl ChainStateStore for MockChainState {
214 async fn get_utxo(
215 &self,
216 _txid: &Txid,
217 _vout: u32,
218 ) -> Result<Option<UtxoEntry>, Box<dyn Error + Send + Sync>> {
219 Ok(None)
220 }
221 async fn has_utxo(
222 &self,
223 _txid: &Txid,
224 _vout: u32,
225 ) -> Result<bool, Box<dyn Error + Send + Sync>> {
226 Ok(false)
227 }
228 async fn write_utxo_set(
229 &self,
230 _adds: Vec<(Txid, u32, UtxoEntry)>,
231 _removes: Vec<(Txid, u32)>,
232 ) -> Result<(), Box<dyn Error + Send + Sync>> {
233 Ok(())
234 }
235 async fn get_best_chain_tip(
236 &self,
237 ) -> Result<(BlockHash, u32), Box<dyn Error + Send + Sync>> {
238 Ok((self.tip_hash, self.tip_height))
239 }
240 async fn write_chain_tip(
241 &self,
242 _hash: BlockHash,
243 _height: u32,
244 ) -> Result<(), Box<dyn Error + Send + Sync>> {
245 Ok(())
246 }
247 async fn get_utxo_set_info(
248 &self,
249 ) -> Result<abtc_ports::UtxoSetInfo, Box<dyn Error + Send + Sync>> {
250 Ok(abtc_ports::UtxoSetInfo {
251 txout_count: 0,
252 total_amount: abtc_domain::primitives::Amount::from_sat(0),
253 best_block: self.tip_hash,
254 height: self.tip_height,
255 })
256 }
257 }
258
259 struct MockMempool {
262 entries: Vec<MempoolEntry>,
263 }
264
265 #[async_trait]
266 impl MempoolPort for MockMempool {
267 async fn add_transaction(
268 &self,
269 _tx: &Transaction,
270 ) -> Result<(), Box<dyn Error + Send + Sync>> {
271 Ok(())
272 }
273 async fn remove_transaction(
274 &self,
275 _txid: &Txid,
276 _recursive: bool,
277 ) -> Result<(), Box<dyn Error + Send + Sync>> {
278 Ok(())
279 }
280 async fn get_transaction(
281 &self,
282 _txid: &Txid,
283 ) -> Result<Option<MempoolEntry>, Box<dyn Error + Send + Sync>> {
284 Ok(None)
285 }
286 async fn get_all_transactions(
287 &self,
288 ) -> Result<Vec<MempoolEntry>, Box<dyn Error + Send + Sync>> {
289 Ok(self.entries.clone())
290 }
291 async fn get_transaction_count(&self) -> Result<u32, Box<dyn Error + Send + Sync>> {
292 Ok(self.entries.len() as u32)
293 }
294 async fn estimate_fee(
295 &self,
296 _target_blocks: u32,
297 ) -> Result<f64, Box<dyn Error + Send + Sync>> {
298 Ok(1.0)
299 }
300 async fn get_mempool_info(&self) -> Result<MempoolInfo, Box<dyn Error + Send + Sync>> {
301 Ok(MempoolInfo {
302 size: self.entries.len() as u32,
303 bytes: 0,
304 usage: 0,
305 max_mempool: 300_000_000,
306 min_relay_fee: 0.00001,
307 })
308 }
309 async fn clear(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
310 Ok(())
311 }
312 }
313
314 fn make_test_tx(value: i64) -> Transaction {
315 let input = TxIn::final_input(OutPoint::new(Txid::zero(), 0), Script::new());
316 let output = TxOut::new(Amount::from_sat(value), Script::new());
317 Transaction::v1(vec![input], vec![output], 0)
318 }
319
320 fn make_mempool_entry(tx: &Transaction, fee: i64) -> MempoolEntry {
321 MempoolEntry {
322 tx: tx.clone(),
323 fee: Amount::from_sat(fee),
324 size: 200,
325 time: 0,
326 height: 0,
327 descendant_count: 0,
328 descendant_size: 0,
329 ancestor_count: 0,
330 ancestor_size: 0,
331 }
332 }
333
334 #[tokio::test]
335 async fn test_create_empty_template() {
336 let chain_state = Arc::new(MockChainState {
337 tip_hash: BlockHash::zero(),
338 tip_height: 0,
339 });
340 let mempool = Arc::new(MockMempool {
341 entries: Vec::new(),
342 });
343
344 let assembler = BlockAssembler::new(chain_state, mempool);
345 let params = ConsensusParams::regtest();
346 let script = Script::new();
347
348 let template = assembler
349 .create_block_template(&script, ¶ms)
350 .await
351 .unwrap();
352
353 assert_eq!(template.block.transactions.len(), 1);
355 assert!(template.block.transactions[0].is_coinbase());
356 assert_eq!(template.height, 1);
357
358 let coinbase_value = template.block.transactions[0].total_output_value();
360 assert_eq!(coinbase_value.as_sat(), 5_000_000_000); }
362
363 #[tokio::test]
364 async fn test_template_includes_mempool_txs() {
365 let chain_state = Arc::new(MockChainState {
366 tip_hash: BlockHash::zero(),
367 tip_height: 100,
368 });
369
370 let tx1 = make_test_tx(50_000);
371 let tx2 = make_test_tx(30_000);
372 let entries = vec![
373 make_mempool_entry(&tx1, 1000),
374 make_mempool_entry(&tx2, 500),
375 ];
376
377 let mempool = Arc::new(MockMempool { entries });
378 let assembler = BlockAssembler::new(chain_state, mempool);
379 let params = ConsensusParams::regtest();
380 let script = Script::new();
381
382 let template = assembler
383 .create_block_template(&script, ¶ms)
384 .await
385 .unwrap();
386
387 assert_eq!(template.block.transactions.len(), 3);
389 assert!(template.block.transactions[0].is_coinbase());
390 assert_eq!(template.height, 101);
391
392 let coinbase_value = template.block.transactions[0].total_output_value();
394 let expected = 5_000_000_000 + 1000 + 500; assert_eq!(coinbase_value.as_sat(), expected);
396 }
397
398 #[tokio::test]
399 async fn test_template_sorts_by_fee_rate() {
400 let chain_state = Arc::new(MockChainState {
401 tip_hash: BlockHash::zero(),
402 tip_height: 0,
403 });
404
405 let tx_low = make_test_tx(10_000);
406 let tx_high = make_test_tx(20_000);
407 let entries = vec![
408 make_mempool_entry(&tx_low, 100), make_mempool_entry(&tx_high, 2000), ];
411
412 let mempool = Arc::new(MockMempool { entries });
413 let assembler = BlockAssembler::new(chain_state, mempool);
414 let params = ConsensusParams::regtest();
415 let script = Script::new();
416
417 let template = assembler
418 .create_block_template(&script, ¶ms)
419 .await
420 .unwrap();
421
422 assert_eq!(template.block.transactions.len(), 3);
424 assert_eq!(template.fees[1].as_sat(), 2000); assert_eq!(template.fees[2].as_sat(), 100); }
427
428 #[tokio::test]
429 async fn test_template_has_valid_merkle_root() {
430 let chain_state = Arc::new(MockChainState {
431 tip_hash: BlockHash::zero(),
432 tip_height: 50,
433 });
434 let mempool = Arc::new(MockMempool {
435 entries: Vec::new(),
436 });
437
438 let assembler = BlockAssembler::new(chain_state, mempool);
439 let params = ConsensusParams::regtest();
440 let script = Script::new();
441
442 let template = assembler
443 .create_block_template(&script, ¶ms)
444 .await
445 .unwrap();
446
447 assert!(template.block.has_valid_merkle_root());
448 }
449
450 #[tokio::test]
451 async fn test_template_subsidy_halving() {
452 let chain_state = Arc::new(MockChainState {
453 tip_hash: BlockHash::zero(),
454 tip_height: 149, });
456 let mempool = Arc::new(MockMempool {
457 entries: Vec::new(),
458 });
459
460 let assembler = BlockAssembler::new(chain_state, mempool);
461 let params = ConsensusParams::regtest();
462 let script = Script::new();
463
464 let template = assembler
465 .create_block_template(&script, ¶ms)
466 .await
467 .unwrap();
468
469 let coinbase_value = template.block.transactions[0].total_output_value();
471 assert_eq!(coinbase_value.as_sat(), 2_500_000_000);
472 }
473
474 #[tokio::test]
475 async fn test_get_block_height() {
476 let chain_state = Arc::new(MockChainState {
477 tip_hash: BlockHash::zero(),
478 tip_height: 42,
479 });
480 let mempool = Arc::new(MockMempool {
481 entries: Vec::new(),
482 });
483
484 let assembler = BlockAssembler::new(chain_state, mempool);
485 let height = assembler.get_block_height().await.unwrap();
486 assert_eq!(height, 43);
487 }
488
489 #[tokio::test]
490 async fn test_coinbase_script_bip34() {
491 let s0 = BlockAssembler::build_coinbase_script(0);
493 assert!(s0.len() >= 2);
494
495 let s1 = BlockAssembler::build_coinbase_script(1);
497 assert!(s1.len() >= 2);
498
499 let s100 = BlockAssembler::build_coinbase_script(100);
501 assert!(s100.len() >= 2);
502 assert_eq!(s100.as_bytes()[0], 1); assert_eq!(s100.as_bytes()[1], 100);
505
506 let s500 = BlockAssembler::build_coinbase_script(500);
508 assert_eq!(s500.as_bytes()[0], 2); }
510}