|
21 | 21 | //! } |
22 | 22 | //! ``` |
23 | 23 |
|
24 | | -use crate::collections::HashMap; |
25 | | -use alloc::sync::Arc; |
| 24 | +use crate::{ |
| 25 | + collections::{HashMap, HashSet}, |
| 26 | + AncestorPackage, |
| 27 | +}; |
| 28 | +use alloc::{ |
| 29 | + collections::{BTreeMap, VecDeque}, |
| 30 | + sync::Arc, |
| 31 | + vec::Vec, |
| 32 | +}; |
26 | 33 | use core::{fmt, ops::RangeBounds}; |
27 | 34 |
|
28 | | -use alloc::vec::Vec; |
29 | | - |
30 | 35 | use bdk_core::BlockId; |
31 | | -use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, Txid}; |
| 36 | +use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, Txid, Weight}; |
32 | 37 |
|
33 | 38 | use crate::{ |
34 | 39 | spk_txout::SpkTxOutIndex, tx_graph::TxNode, Anchor, Balance, CanonicalIter, CanonicalReason, |
@@ -448,4 +453,134 @@ impl<A: Anchor> CanonicalView<A> { |
448 | 453 | .collect() |
449 | 454 | }) |
450 | 455 | } |
| 456 | + |
| 457 | + /// Compute ancestor packages for all unconfirmed and unspent outpoints. |
| 458 | + /// |
| 459 | + /// Returns a map from [`OutPoint`] to its [`AncestorPackage`]. |
| 460 | + pub fn ancestor_packages(&self) -> BTreeMap<OutPoint, AncestorPackage> { |
| 461 | + let mut fee_cache: HashMap<Txid, Option<Amount>> = HashMap::new(); |
| 462 | + let mut package_cache: HashMap<Txid, Option<(Weight, Amount)>> = HashMap::new(); |
| 463 | + let mut result = BTreeMap::new(); |
| 464 | + |
| 465 | + for (&txid, (tx, pos)) in &self.txs { |
| 466 | + if pos.is_confirmed() { |
| 467 | + continue; |
| 468 | + } |
| 469 | + |
| 470 | + for (vout, _) in tx.output.iter().enumerate() { |
| 471 | + let outpoint = OutPoint::new(txid, vout as u32); |
| 472 | + |
| 473 | + if self.spends.contains_key(&outpoint) { |
| 474 | + continue; |
| 475 | + } |
| 476 | + |
| 477 | + let pkg = match package_cache.get(&txid) { |
| 478 | + Some(cached) => *cached, |
| 479 | + None => { |
| 480 | + let pkg = self.compute_package(core::iter::once(txid), &mut fee_cache); |
| 481 | + package_cache.insert(txid, pkg); |
| 482 | + pkg |
| 483 | + } |
| 484 | + }; |
| 485 | + |
| 486 | + if let Some((weight, fee)) = pkg { |
| 487 | + result.insert(outpoint, AncestorPackage::new(weight, fee)); |
| 488 | + } |
| 489 | + } |
| 490 | + } |
| 491 | + |
| 492 | + result |
| 493 | + } |
| 494 | + |
| 495 | + /// Compute a deduplicated ancestor package for a set of outpoints. |
| 496 | + /// |
| 497 | + /// Each ancestor txid is counted exactly once across all outpoints. |
| 498 | + /// Used after coin selection to verify the true aggregate deficit. |
| 499 | + pub fn aggregate_ancestor_package( |
| 500 | + &self, |
| 501 | + outpoints: impl IntoIterator<Item = OutPoint>, |
| 502 | + ) -> Option<AncestorPackage> { |
| 503 | + let mut fee_cache = HashMap::new(); |
| 504 | + let (weight, fee) = |
| 505 | + self.compute_package(outpoints.into_iter().map(|op| op.txid), &mut fee_cache)?; |
| 506 | + |
| 507 | + if weight == Weight::ZERO { |
| 508 | + return None; |
| 509 | + } |
| 510 | + |
| 511 | + Some(AncestorPackage::new(weight, fee)) |
| 512 | + } |
| 513 | + |
| 514 | + /// Compute the aggregate `(weight, fee)` for the unconfirmed ancestor |
| 515 | + /// chain rooted at `txid`, including `txid` itself. |
| 516 | + /// |
| 517 | + /// Returns `None` if any ancestor's fee cannot be computed. |
| 518 | + fn compute_package( |
| 519 | + &self, |
| 520 | + txids: impl IntoIterator<Item = Txid>, |
| 521 | + fee_cache: &mut HashMap<Txid, Option<Amount>>, |
| 522 | + ) -> Option<(Weight, Amount)> { |
| 523 | + let mut visited = HashSet::new(); |
| 524 | + let mut total_weight = Weight::ZERO; |
| 525 | + let mut total_fee = Amount::ZERO; |
| 526 | + let mut queue = VecDeque::new(); |
| 527 | + |
| 528 | + for txid in txids { |
| 529 | + queue.push_back(txid); |
| 530 | + } |
| 531 | + |
| 532 | + while let Some(current) = queue.pop_front() { |
| 533 | + if !visited.insert(current) { |
| 534 | + continue; |
| 535 | + } |
| 536 | + |
| 537 | + let (tx, pos) = match self.txs.get(¤t) { |
| 538 | + Some(entry) => entry, |
| 539 | + None => continue, |
| 540 | + }; |
| 541 | + |
| 542 | + // Confirmed txs don't need fee bumping. |
| 543 | + if pos.is_confirmed() { |
| 544 | + continue; |
| 545 | + } |
| 546 | + |
| 547 | + let fee = match fee_cache.get(¤t) { |
| 548 | + Some(cached) => *cached, |
| 549 | + None => { |
| 550 | + let fee = self.package_tx_fee(tx); |
| 551 | + fee_cache.insert(current, fee); |
| 552 | + fee |
| 553 | + } |
| 554 | + }; |
| 555 | + |
| 556 | + total_fee += fee?; |
| 557 | + total_weight += tx.weight(); |
| 558 | + |
| 559 | + for txin in &tx.input { |
| 560 | + queue.push_back(txin.previous_output.txid); |
| 561 | + } |
| 562 | + } |
| 563 | + |
| 564 | + Some((total_weight, total_fee)) |
| 565 | + } |
| 566 | + |
| 567 | + /// Compute the fee for a transaction. |
| 568 | + /// |
| 569 | + /// Returns `None` if any input's previous output is not found. |
| 570 | + fn package_tx_fee(&self, tx: &Transaction) -> Option<Amount> { |
| 571 | + if tx.is_coinbase() { |
| 572 | + return Some(Amount::ZERO); |
| 573 | + } |
| 574 | + |
| 575 | + let inputs_sum = tx.input.iter().try_fold(Amount::ZERO, |sum, txin| { |
| 576 | + let prev_op = txin.previous_output; |
| 577 | + let (parent_tx, _) = self.txs.get(&prev_op.txid)?; |
| 578 | + let txout = parent_tx.output.get(prev_op.vout as usize)?; |
| 579 | + Some(sum + txout.value) |
| 580 | + })?; |
| 581 | + |
| 582 | + let outputs_sum: Amount = tx.output.iter().map(|o| o.value).sum(); |
| 583 | + |
| 584 | + inputs_sum.checked_sub(outputs_sum) |
| 585 | + } |
451 | 586 | } |
0 commit comments