Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/bitcoind_rpc/tests/test_emitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ fn get_balance(
outpoints.into_iter().map(|(_, op)| op),
bdk_chain::taints_unowned(&recv_graph.index),
|pos| pos.is_confirmed(),
|_txout| false,
);
Ok(balance)
}
Expand Down
1 change: 1 addition & 0 deletions crates/chain/benches/indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ fn do_bench(indexed_tx_graph: &KeychainTxGraph, chain: &LocalChain) {
op.into_iter().map(|(_, o)| o),
bdk_chain::taints_unowned(&graph.index),
|pos| pos.is_confirmed(),
|_txout| false,
);
assert_eq!(bal.total(), AMOUNT * TX_CT as u64);
}
Expand Down
18 changes: 12 additions & 6 deletions crates/chain/benches/trust_classification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,17 @@ fn make_chain() -> LocalChain {
LocalChain::from_blocks(blocks).unwrap()
}

// The two predicates `classify_outpoints` takes, faking what a wallet would pass:
// `does_taint` = the tx pulls in coins that aren't ours, `is_settled` = the tx is confirmed.
// The three predicates `classify_outpoints` takes, faking what a wallet would pass:
// `does_taint` = the tx pulls in coins that aren't ours; `is_settled` = the tx is confirmed;
// `is_locked` = the tx has timelocked outputs.
#[allow(clippy::type_complexity)]
fn does_taint_and_is_settled<'a>(
fn classify_outpoints_predicates<'a>(
view: &'a CanonicalView<ConfirmationBlockTime>,
owned: &ScriptBuf,
) -> (
impl FnMut(&bdk_chain::CanonicalTx<ChainPosition<ConfirmationBlockTime>>) -> bool + 'a,
impl Fn(&ChainPosition<ConfirmationBlockTime>) -> bool,
impl Fn(&bdk_chain::CanonicalTxOut<ChainPosition<ConfirmationBlockTime>>) -> bool,
) {
let owned = owned.clone();
let is_mine = move |spk: &Script| spk == owned.as_script();
Expand All @@ -47,7 +49,11 @@ fn does_taint_and_is_settled<'a>(
};
let is_settled =
|pos: &ChainPosition<ConfirmationBlockTime>| matches!(pos, ChainPosition::Confirmed { .. });
(does_taint, is_settled)

let is_locked =
|_txout: &bdk_chain::CanonicalTxOut<ChainPosition<ConfirmationBlockTime>>| false;

(does_taint, is_settled, is_locked)
}

// Per-UTXO memoized classification (`classify_outpoints`).
Expand All @@ -57,8 +63,8 @@ fn run_classify(
owned: &ScriptBuf,
) {
let outpoints = utxo_txids.iter().map(|&txid| OutPoint::new(txid, 0));
let (does_taint, is_settled) = does_taint_and_is_settled(view, owned);
for item in view.classify_outpoints(outpoints, does_taint, is_settled) {
let (does_taint, is_settled, is_locked) = classify_outpoints_predicates(view, owned);
for item in view.classify_outpoints(outpoints, does_taint, is_settled, is_locked) {
std::hint::black_box(item);
}
}
Expand Down
9 changes: 6 additions & 3 deletions crates/chain/src/balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub struct Balance {
pub untrusted_pending: Amount,
/// Confirmed and immediately spendable balance
pub confirmed: Amount,
/// Coins locked by some timelock
pub locked: Amount,
}

impl Balance {
Expand All @@ -25,16 +27,16 @@ impl Balance {

/// Get the whole balance visible to the wallet.
pub fn total(&self) -> Amount {
self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature
self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature + self.locked
}
}

impl core::fmt::Display for Balance {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{{ immature: {}, trusted_pending: {}, untrusted_pending: {}, confirmed: {} }}",
self.immature, self.trusted_pending, self.untrusted_pending, self.confirmed
"{{ immature: {}, trusted_pending: {}, untrusted_pending: {}, confirmed: {}, locked: {} }}",
self.immature, self.trusted_pending, self.untrusted_pending, self.confirmed, self.locked
)
}
}
Expand All @@ -48,6 +50,7 @@ impl core::ops::Add for Balance {
trusted_pending: self.trusted_pending + other.trusted_pending,
untrusted_pending: self.untrusted_pending + other.untrusted_pending,
confirmed: self.confirmed + other.confirmed,
locked: self.locked + other.locked,
}
}
}
16 changes: 14 additions & 2 deletions crates/chain/src/canonical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ pub enum Eligibility {
Immature,
/// An output not yet settled.
Unsettled(Trust),
/// Locked outputs by a timelock.
Locked,
}

/// Describes whether an [`Unsettled`](Eligibility::Unsettled) output is trusted, untrusted, or of
Expand Down Expand Up @@ -436,11 +438,13 @@ impl<A: Anchor> CanonicalView<A> {
/// [`taints_unowned`] to classify everything foreign as untrusted.
/// * `is_settled` - Returns `true` for the [position](ChainPosition) of a transaction we
/// consider settled (unlikely to be replaced), for example one with enough confirmations.
/// * `is_locked` - Returns `true` for a txout locked by consensus.
pub fn classify_outpoints<'a>(
&'a self,
outpoints: impl IntoIterator<Item = OutPoint> + 'a,
mut does_taint: impl FnMut(&CanonicalTx<ChainPosition<A>>) -> bool + 'a,
is_settled: impl Fn(&ChainPosition<A>) -> bool + 'a,
is_locked: impl Fn(&CanonicalTxOut<ChainPosition<A>>) -> bool + 'a,
) -> impl Iterator<Item = (CanonicalTxOut<ChainPosition<A>>, Eligibility)> + 'a {
let tip = self.tip.height;
// Shared across outpoints so an ancestor reached by several of them is only walked once.
Expand All @@ -453,7 +457,11 @@ impl<A: Anchor> CanonicalView<A> {
let eligibility = if !txout.is_mature(tip) {
Eligibility::Immature
} else if is_settled(&txout.pos) {
Eligibility::Settled
if is_locked(&txout) {
Eligibility::Locked
} else {
Eligibility::Settled
}
} else {
Eligibility::Unsettled(self.ancestry_trust(
txout.outpoint.txid,
Expand All @@ -462,6 +470,7 @@ impl<A: Anchor> CanonicalView<A> {
&mut cache,
))
};

(txout, eligibility)
})
}
Expand Down Expand Up @@ -590,15 +599,17 @@ impl<A: Anchor> CanonicalView<A> {
/// pos.confirmation_height_upper_bound()
/// .is_some_and(|h| tip_height.saturating_sub(h).saturating_add(1) >= 6)
/// },
/// |_txout| false,
/// );
/// ```
pub fn balance(
&self,
outpoints: impl IntoIterator<Item = OutPoint>,
does_taint: impl FnMut(&CanonicalTx<ChainPosition<A>>) -> bool,
is_settled: impl Fn(&ChainPosition<A>) -> bool,
is_locked: impl Fn(&CanonicalTxOut<ChainPosition<A>>) -> bool,
) -> Balance {
self.classify_outpoints(outpoints, does_taint, is_settled)
self.classify_outpoints(outpoints, does_taint, is_settled, is_locked)
.collect()
}
}
Expand All @@ -617,6 +628,7 @@ impl<A: Anchor> FromIterator<(CanonicalTxOut<ChainPosition<A>>, Eligibility)> fo
Eligibility::Unsettled(Trust::Untrusted | Trust::Unknown) => {
&mut balance.untrusted_pending
}
Eligibility::Locked => &mut balance.locked,
};
*bucket += txout.txout.value;
}
Expand Down
Loading
Loading