1use abtc_domain::policy::packages::{self, PackageError, PackageType, MAX_PACKAGE_VSIZE};
29use abtc_domain::primitives::{Amount, Transaction, Txid};
30use abtc_ports::{ChainStateStore, MempoolPort};
31use std::collections::HashMap;
32use std::sync::Arc;
33
34#[derive(Debug, Clone)]
36pub struct PackageResult {
37 pub accepted: Vec<PackageAcceptedTx>,
39 pub package_type: PackageType,
41 pub total_fee: Amount,
43 pub total_vsize: u32,
45 pub package_fee_rate: f64,
47}
48
49#[derive(Debug, Clone)]
51pub struct PackageAcceptedTx {
52 pub txid: Txid,
54 pub fee: Amount,
56 pub vsize: u32,
58}
59
60#[derive(Debug)]
62pub enum PackageAcceptError {
63 PackageValidation(PackageError),
65 MissingInput { txid: Txid, detail: String },
67 NegativeFee { txid: Txid },
69 InsufficientPackageFeeRate { fee_rate: f64, min_rate: f64 },
71 MempoolRejection { txid: Txid, reason: String },
73 PackageTooLarge { vsize: u32, limit: u32 },
75}
76
77impl std::fmt::Display for PackageAcceptError {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 match self {
80 PackageAcceptError::PackageValidation(e) => write!(f, "package validation: {}", e),
81 PackageAcceptError::MissingInput { txid, detail } => {
82 write!(f, "missing input for tx {}: {}", txid, detail)
83 }
84 PackageAcceptError::NegativeFee { txid } => {
85 write!(f, "tx {} has negative fee", txid)
86 }
87 PackageAcceptError::InsufficientPackageFeeRate { fee_rate, min_rate } => {
88 write!(
89 f,
90 "package fee rate {:.2} sat/vB below minimum {:.2}",
91 fee_rate, min_rate
92 )
93 }
94 PackageAcceptError::MempoolRejection { txid, reason } => {
95 write!(f, "mempool rejected tx {}: {}", txid, reason)
96 }
97 PackageAcceptError::PackageTooLarge { vsize, limit } => {
98 write!(f, "package {} vB exceeds limit {} vB", vsize, limit)
99 }
100 }
101 }
102}
103
104impl std::error::Error for PackageAcceptError {}
105
106pub struct PackageAcceptor {
111 chain_state: Arc<dyn ChainStateStore>,
112 mempool: Arc<dyn MempoolPort>,
113 verify_scripts: bool,
115}
116
117impl PackageAcceptor {
118 pub fn new(chain_state: Arc<dyn ChainStateStore>, mempool: Arc<dyn MempoolPort>) -> Self {
120 PackageAcceptor {
121 chain_state,
122 mempool,
123 verify_scripts: true,
124 }
125 }
126
127 pub fn set_verify_scripts(&mut self, verify: bool) {
129 self.verify_scripts = verify;
130 }
131
132 pub async fn accept_package(
137 &self,
138 transactions: &[Transaction],
139 ) -> Result<PackageResult, PackageAcceptError> {
140 let package_type = packages::validate_package(transactions)
142 .map_err(PackageAcceptError::PackageValidation)?;
143
144 let mut package_outputs: HashMap<(Txid, u32), Amount> = HashMap::new();
148 let mut tx_fees: Vec<(Txid, Amount, u32)> = Vec::new();
149 let mut total_fee = Amount::from_sat(0);
150 let mut total_vsize: u32 = 0;
151
152 for tx in transactions {
153 let txid = tx.txid();
154 let vsize = packages::estimate_package_tx_vsize(tx);
155
156 let mut input_total: i64 = 0;
158 for input in &tx.inputs {
159 let prev_txid = input.previous_output.txid;
160 let prev_vout = input.previous_output.vout;
161
162 if let Some(value) = package_outputs.get(&(prev_txid, prev_vout)) {
164 input_total += value.as_sat();
165 } else {
166 let utxo = self
168 .chain_state
169 .get_utxo(&prev_txid, prev_vout)
170 .await
171 .map_err(|e| PackageAcceptError::MissingInput {
172 txid,
173 detail: e.to_string(),
174 })?
175 .ok_or_else(|| PackageAcceptError::MissingInput {
176 txid,
177 detail: format!("{}:{}", prev_txid, prev_vout),
178 })?;
179 input_total += utxo.output.value.as_sat();
180 }
181 }
182
183 let output_total: i64 = tx.outputs.iter().map(|o| o.value.as_sat()).sum();
184
185 if input_total < output_total {
186 return Err(PackageAcceptError::NegativeFee { txid });
187 }
188
189 let fee = Amount::from_sat(input_total - output_total);
190
191 for (vout, output) in tx.outputs.iter().enumerate() {
193 package_outputs.insert((txid, vout as u32), output.value);
194 }
195
196 total_fee = Amount::from_sat(total_fee.as_sat() + fee.as_sat());
197 total_vsize += vsize;
198 tx_fees.push((txid, fee, vsize));
199 }
200
201 if total_vsize > MAX_PACKAGE_VSIZE {
203 return Err(PackageAcceptError::PackageTooLarge {
204 vsize: total_vsize,
205 limit: MAX_PACKAGE_VSIZE,
206 });
207 }
208
209 let package_fee_rate =
211 packages::check_package_fee_rate(total_fee, total_vsize).map_err(|e| match e {
212 PackageError::InsufficientPackageFeeRate { fee_rate, min_rate } => {
213 PackageAcceptError::InsufficientPackageFeeRate { fee_rate, min_rate }
214 }
215 _ => PackageAcceptError::PackageValidation(e),
216 })?;
217
218 let mut accepted = Vec::new();
220
221 for (i, tx) in transactions.iter().enumerate() {
222 let (txid, fee, vsize) = &tx_fees[i];
223
224 self.mempool.add_transaction(tx).await.map_err(|e| {
225 PackageAcceptError::MempoolRejection {
226 txid: *txid,
227 reason: e.to_string(),
228 }
229 })?;
230
231 tracing::info!(
232 "Package: accepted tx {} ({}/{}, fee={}, vsize={})",
233 txid,
234 i + 1,
235 transactions.len(),
236 fee.as_sat(),
237 vsize,
238 );
239
240 accepted.push(PackageAcceptedTx {
241 txid: *txid,
242 fee: *fee,
243 vsize: *vsize,
244 });
245 }
246
247 tracing::info!(
248 "Package accepted: {} txs, total_fee={}, total_vsize={}, rate={:.1} sat/vB, type={:?}",
249 accepted.len(),
250 total_fee.as_sat(),
251 total_vsize,
252 package_fee_rate,
253 package_type,
254 );
255
256 Ok(PackageResult {
257 accepted,
258 package_type,
259 total_fee,
260 total_vsize,
261 package_fee_rate,
262 })
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use abtc_domain::primitives::{BlockHash, Hash256, OutPoint, TxIn, TxOut};
270 use abtc_domain::Script;
271 use abtc_ports::{MempoolEntry, MempoolInfo, UtxoEntry, UtxoSetInfo};
272 use async_trait::async_trait;
273 use std::collections::HashMap;
274 use tokio::sync::RwLock;
275
276 struct MockChainState {
279 utxos: RwLock<HashMap<(Txid, u32), UtxoEntry>>,
280 }
281
282 impl MockChainState {
283 fn new() -> Self {
284 MockChainState {
285 utxos: RwLock::new(HashMap::new()),
286 }
287 }
288
289 async fn add_utxo(&self, txid: Txid, vout: u32, entry: UtxoEntry) {
290 self.utxos.write().await.insert((txid, vout), entry);
291 }
292 }
293
294 #[async_trait]
295 impl ChainStateStore for MockChainState {
296 async fn get_utxo(
297 &self,
298 txid: &Txid,
299 vout: u32,
300 ) -> Result<Option<UtxoEntry>, Box<dyn std::error::Error + Send + Sync>> {
301 Ok(self.utxos.read().await.get(&(*txid, vout)).cloned())
302 }
303
304 async fn has_utxo(
305 &self,
306 txid: &Txid,
307 vout: u32,
308 ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
309 Ok(self.utxos.read().await.contains_key(&(*txid, vout)))
310 }
311
312 async fn write_utxo_set(
313 &self,
314 _adds: Vec<(Txid, u32, UtxoEntry)>,
315 _removes: Vec<(Txid, u32)>,
316 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
317 Ok(())
318 }
319
320 async fn get_best_chain_tip(
321 &self,
322 ) -> Result<(BlockHash, u32), Box<dyn std::error::Error + Send + Sync>> {
323 Ok((BlockHash::zero(), 100))
324 }
325
326 async fn write_chain_tip(
327 &self,
328 _hash: BlockHash,
329 _height: u32,
330 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
331 Ok(())
332 }
333
334 async fn get_utxo_set_info(
335 &self,
336 ) -> Result<UtxoSetInfo, Box<dyn std::error::Error + Send + Sync>> {
337 Ok(UtxoSetInfo {
338 txout_count: 0,
339 total_amount: Amount::from_sat(0),
340 best_block: BlockHash::zero(),
341 height: 100,
342 })
343 }
344 }
345
346 struct MockMempool {
349 txs: RwLock<HashMap<Txid, Transaction>>,
350 }
351
352 impl MockMempool {
353 fn new() -> Self {
354 MockMempool {
355 txs: RwLock::new(HashMap::new()),
356 }
357 }
358 }
359
360 #[async_trait]
361 impl MempoolPort for MockMempool {
362 async fn add_transaction(
363 &self,
364 tx: &Transaction,
365 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
366 let txid = tx.txid();
367 let mut txs = self.txs.write().await;
368 if txs.contains_key(&txid) {
369 return Err("already in mempool".into());
370 }
371 txs.insert(txid, tx.clone());
372 Ok(())
373 }
374
375 async fn remove_transaction(
376 &self,
377 txid: &Txid,
378 _recursive: bool,
379 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
380 self.txs.write().await.remove(txid);
381 Ok(())
382 }
383
384 async fn get_transaction(
385 &self,
386 txid: &Txid,
387 ) -> Result<Option<MempoolEntry>, Box<dyn std::error::Error + Send + Sync>> {
388 let txs = self.txs.read().await;
389 Ok(txs.get(txid).map(|tx| MempoolEntry {
390 tx: tx.clone(),
391 fee: Amount::from_sat(0),
392 size: 100,
393 time: 0,
394 height: 0,
395 descendant_count: 0,
396 descendant_size: 0,
397 ancestor_count: 0,
398 ancestor_size: 0,
399 }))
400 }
401
402 async fn get_all_transactions(
403 &self,
404 ) -> Result<Vec<MempoolEntry>, Box<dyn std::error::Error + Send + Sync>> {
405 Ok(vec![])
406 }
407
408 async fn get_transaction_count(
409 &self,
410 ) -> Result<u32, Box<dyn std::error::Error + Send + Sync>> {
411 Ok(self.txs.read().await.len() as u32)
412 }
413
414 async fn estimate_fee(
415 &self,
416 _target_blocks: u32,
417 ) -> Result<f64, Box<dyn std::error::Error + Send + Sync>> {
418 Ok(1.0)
419 }
420
421 async fn get_mempool_info(
422 &self,
423 ) -> Result<MempoolInfo, Box<dyn std::error::Error + Send + Sync>> {
424 Ok(MempoolInfo {
425 size: 0,
426 bytes: 0,
427 usage: 0,
428 max_mempool: 300_000_000,
429 min_relay_fee: 0.00001,
430 })
431 }
432
433 async fn clear(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
434 self.txs.write().await.clear();
435 Ok(())
436 }
437 }
438
439 fn funding_txid(byte: u8) -> Txid {
442 Txid::from_hash(Hash256::from_bytes([byte; 32]))
443 }
444
445 fn make_utxo(value: i64) -> UtxoEntry {
446 UtxoEntry {
447 output: TxOut::new(Amount::from_sat(value), Script::new()),
448 height: 1,
449 is_coinbase: false,
450 }
451 }
452
453 #[tokio::test]
456 async fn test_accept_simple_package() {
457 let chain_state = Arc::new(MockChainState::new());
458 let mempool = Arc::new(MockMempool::new());
459
460 let ftxid = funding_txid(0x01);
462 chain_state.add_utxo(ftxid, 0, make_utxo(100_000)).await;
463
464 let parent = Transaction::v1(
465 vec![TxIn::final_input(OutPoint::new(ftxid, 0), Script::new())],
466 vec![TxOut::new(Amount::from_sat(90_000), Script::new())],
467 0,
468 );
469 let parent_txid = parent.txid();
470
471 let child = Transaction::v1(
472 vec![TxIn::final_input(
473 OutPoint::new(parent_txid, 0),
474 Script::new(),
475 )],
476 vec![TxOut::new(Amount::from_sat(80_000), Script::new())],
477 0,
478 );
479
480 let mut acceptor = PackageAcceptor::new(chain_state, mempool.clone());
481 acceptor.set_verify_scripts(false);
482
483 let result = acceptor.accept_package(&[parent, child]).await.unwrap();
484
485 assert_eq!(result.accepted.len(), 2);
486 assert_eq!(result.package_type, PackageType::ChildWithParents);
487 assert_eq!(result.total_fee.as_sat(), 20_000); assert!(result.package_fee_rate > 0.0);
489
490 assert_eq!(mempool.get_transaction_count().await.unwrap(), 2);
492 }
493
494 #[tokio::test]
495 async fn test_cpfp_low_parent_high_child() {
496 let chain_state = Arc::new(MockChainState::new());
497 let mempool = Arc::new(MockMempool::new());
498
499 let ftxid = funding_txid(0x02);
500 chain_state.add_utxo(ftxid, 0, make_utxo(100_000)).await;
501
502 let parent = Transaction::v1(
504 vec![TxIn::final_input(OutPoint::new(ftxid, 0), Script::new())],
505 vec![TxOut::new(Amount::from_sat(99_900), Script::new())],
506 0,
507 );
508 let parent_txid = parent.txid();
509
510 let child = Transaction::v1(
512 vec![TxIn::final_input(
513 OutPoint::new(parent_txid, 0),
514 Script::new(),
515 )],
516 vec![TxOut::new(Amount::from_sat(89_900), Script::new())],
517 0,
518 );
519
520 let mut acceptor = PackageAcceptor::new(chain_state, mempool.clone());
521 acceptor.set_verify_scripts(false);
522
523 let result = acceptor.accept_package(&[parent, child]).await.unwrap();
524
525 assert_eq!(result.total_fee.as_sat(), 10_100);
529 assert!(result.package_fee_rate > 1.0); }
531
532 #[tokio::test]
533 async fn test_missing_input_rejected() {
534 let chain_state = Arc::new(MockChainState::new());
535 let mempool = Arc::new(MockMempool::new());
536
537 let parent = Transaction::v1(
539 vec![TxIn::final_input(
540 OutPoint::new(funding_txid(0x99), 0),
541 Script::new(),
542 )],
543 vec![TxOut::new(Amount::from_sat(50_000), Script::new())],
544 0,
545 );
546
547 let mut acceptor = PackageAcceptor::new(chain_state, mempool);
548 acceptor.set_verify_scripts(false);
549
550 let result = acceptor.accept_package(&[parent]).await;
551 assert!(result.is_err());
552 assert!(matches!(
553 result.unwrap_err(),
554 PackageAcceptError::MissingInput { .. }
555 ));
556 }
557
558 #[tokio::test]
559 async fn test_empty_package_rejected() {
560 let chain_state = Arc::new(MockChainState::new());
561 let mempool = Arc::new(MockMempool::new());
562
563 let acceptor = PackageAcceptor::new(chain_state, mempool);
564 let result = acceptor.accept_package(&[]).await;
565 assert!(result.is_err());
566 assert!(matches!(
567 result.unwrap_err(),
568 PackageAcceptError::PackageValidation(_)
569 ));
570 }
571
572 #[tokio::test]
573 async fn test_negative_fee_rejected() {
574 let chain_state = Arc::new(MockChainState::new());
575 let mempool = Arc::new(MockMempool::new());
576
577 let ftxid = funding_txid(0x03);
578 chain_state.add_utxo(ftxid, 0, make_utxo(1_000)).await;
579
580 let tx = Transaction::v1(
582 vec![TxIn::final_input(OutPoint::new(ftxid, 0), Script::new())],
583 vec![TxOut::new(Amount::from_sat(2_000), Script::new())],
584 0,
585 );
586
587 let mut acceptor = PackageAcceptor::new(chain_state, mempool);
588 acceptor.set_verify_scripts(false);
589
590 let result = acceptor.accept_package(&[tx]).await;
591 assert!(result.is_err());
592 assert!(matches!(
593 result.unwrap_err(),
594 PackageAcceptError::NegativeFee { .. }
595 ));
596 }
597
598 #[tokio::test]
599 async fn test_two_parents_one_child_package() {
600 let chain_state = Arc::new(MockChainState::new());
601 let mempool = Arc::new(MockMempool::new());
602
603 let ftxid_a = funding_txid(0x10);
604 let ftxid_b = funding_txid(0x11);
605 chain_state.add_utxo(ftxid_a, 0, make_utxo(50_000)).await;
606 chain_state.add_utxo(ftxid_b, 0, make_utxo(50_000)).await;
607
608 let parent_a = Transaction::v1(
609 vec![TxIn::final_input(OutPoint::new(ftxid_a, 0), Script::new())],
610 vec![TxOut::new(Amount::from_sat(49_000), Script::new())],
611 0,
612 );
613 let parent_a_txid = parent_a.txid();
614
615 let parent_b = Transaction::v1(
616 vec![TxIn::final_input(OutPoint::new(ftxid_b, 0), Script::new())],
617 vec![TxOut::new(Amount::from_sat(49_000), Script::new())],
618 0,
619 );
620 let parent_b_txid = parent_b.txid();
621
622 let child = Transaction::v1(
623 vec![
624 TxIn::final_input(OutPoint::new(parent_a_txid, 0), Script::new()),
625 TxIn::final_input(OutPoint::new(parent_b_txid, 0), Script::new()),
626 ],
627 vec![TxOut::new(Amount::from_sat(90_000), Script::new())],
628 0,
629 );
630
631 let mut acceptor = PackageAcceptor::new(chain_state, mempool.clone());
632 acceptor.set_verify_scripts(false);
633
634 let result = acceptor
635 .accept_package(&[parent_a, parent_b, child])
636 .await
637 .unwrap();
638
639 assert_eq!(result.accepted.len(), 3);
640 assert_eq!(result.package_type, PackageType::ChildWithParents);
641 assert_eq!(result.total_fee.as_sat(), 10_000);
643 assert_eq!(mempool.get_transaction_count().await.unwrap(), 3);
644 }
645
646 #[tokio::test]
649 async fn regression_child_references_parent_output() {
650 let chain_state = Arc::new(MockChainState::new());
653 let mempool = Arc::new(MockMempool::new());
654
655 let ftxid = funding_txid(0x20);
656 chain_state.add_utxo(ftxid, 0, make_utxo(100_000)).await;
657
658 let parent = Transaction::v1(
659 vec![TxIn::final_input(OutPoint::new(ftxid, 0), Script::new())],
660 vec![
661 TxOut::new(Amount::from_sat(40_000), Script::new()),
662 TxOut::new(Amount::from_sat(50_000), Script::new()),
663 ],
664 0,
665 );
666 let parent_txid = parent.txid();
667
668 let child = Transaction::v1(
670 vec![TxIn::final_input(
671 OutPoint::new(parent_txid, 1),
672 Script::new(),
673 )],
674 vec![TxOut::new(Amount::from_sat(45_000), Script::new())],
675 0,
676 );
677
678 let mut acceptor = PackageAcceptor::new(chain_state, mempool.clone());
679 acceptor.set_verify_scripts(false);
680
681 let result = acceptor.accept_package(&[parent, child]).await.unwrap();
682
683 assert_eq!(result.total_fee.as_sat(), 15_000);
686 assert_eq!(result.accepted.len(), 2);
687 }
688
689 #[tokio::test]
690 async fn regression_duplicate_mempool_submission_fails() {
691 let chain_state = Arc::new(MockChainState::new());
694 let mempool = Arc::new(MockMempool::new());
695
696 let ftxid = funding_txid(0x30);
697 chain_state.add_utxo(ftxid, 0, make_utxo(100_000)).await;
698
699 let tx = Transaction::v1(
700 vec![TxIn::final_input(OutPoint::new(ftxid, 0), Script::new())],
701 vec![TxOut::new(Amount::from_sat(90_000), Script::new())],
702 0,
703 );
704
705 mempool.add_transaction(&tx).await.unwrap();
707
708 let mut acceptor = PackageAcceptor::new(chain_state, mempool);
709 acceptor.set_verify_scripts(false);
710
711 let result = acceptor.accept_package(&[tx]).await;
712 assert!(result.is_err());
713 assert!(matches!(
714 result.unwrap_err(),
715 PackageAcceptError::MempoolRejection { .. }
716 ));
717 }
718
719 #[tokio::test]
720 async fn regression_package_fee_components() {
721 let chain_state = Arc::new(MockChainState::new());
723 let mempool = Arc::new(MockMempool::new());
724
725 let ftxid = funding_txid(0x40);
726 chain_state.add_utxo(ftxid, 0, make_utxo(100_000)).await;
727
728 let parent = Transaction::v1(
729 vec![TxIn::final_input(OutPoint::new(ftxid, 0), Script::new())],
730 vec![TxOut::new(Amount::from_sat(95_000), Script::new())],
731 0,
732 );
733 let parent_txid = parent.txid();
734
735 let child = Transaction::v1(
736 vec![TxIn::final_input(
737 OutPoint::new(parent_txid, 0),
738 Script::new(),
739 )],
740 vec![TxOut::new(Amount::from_sat(85_000), Script::new())],
741 0,
742 );
743
744 let mut acceptor = PackageAcceptor::new(chain_state, mempool);
745 acceptor.set_verify_scripts(false);
746
747 let result = acceptor.accept_package(&[parent, child]).await.unwrap();
748
749 assert_eq!(result.accepted[0].fee.as_sat(), 5_000);
751 assert_eq!(result.accepted[1].fee.as_sat(), 10_000);
753 assert_eq!(result.total_fee.as_sat(), 15_000);
755 }
756}