Skip to content

Commit b5b29ed

Browse files
committed
feat(chain): add ancestor package computation to CanonicalView
1 parent bc171c8 commit b5b29ed

File tree

1 file changed

+140
-5
lines changed

1 file changed

+140
-5
lines changed

crates/chain/src/canonical_view.rs

Lines changed: 140 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,19 @@
2121
//! }
2222
//! ```
2323
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+
};
2633
use core::{fmt, ops::RangeBounds};
2734

28-
use alloc::vec::Vec;
29-
3035
use bdk_core::BlockId;
31-
use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, Txid};
36+
use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, Txid, Weight};
3237

3338
use crate::{
3439
spk_txout::SpkTxOutIndex, tx_graph::TxNode, Anchor, Balance, CanonicalIter, CanonicalReason,
@@ -448,4 +453,134 @@ impl<A: Anchor> CanonicalView<A> {
448453
.collect()
449454
})
450455
}
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(&current) {
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(&current) {
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+
}
451586
}

0 commit comments

Comments
 (0)