Skip to content

CanonicalView::balance misclassifies immature coinbase outputs when min_confirmations exceeds their confirmations #2267

Description

@Amanyadav0556

Describe the bug

CanonicalView::balance can misclassify an immature confirmed coinbase output as trusted_pending or untrusted_pending when its current confirmation count is below the caller-provided min_confirmations.

The issue is in crates/chain/src/canonical.rs, inside the ChainPosition::Confirmed branch of CanonicalView::balance.

The current logic checks confirmations < min_confirmations before checking whether the output is mature:

if confirmations < min_confirmations {
    if trust_predicate(&spk_i, &txout) {
        trusted_pending += txout.txout.value;
    } else {
        untrusted_pending += txout.txout.value;
    }
} else if txout.is_confirmed_and_spendable(self.tip.height) {
    confirmed += txout.txout.value;
} else if !txout.is_mature(self.tip.height) {
    immature += txout.txout.value;
}

This means that when an immature coinbase satisfies:

confirmations < min_confirmations

the first branch is taken and the maturity check is never reached.

As a result, the immature coinbase value is placed into trusted_pending or untrusted_pending instead of Balance::immature.

This can also affect the semantics of Balance::trusted_spendable(), since trusted_pending contributes to that value while an immature coinbase output is not yet spendable.


To Reproduce

Consider a confirmed coinbase output with:

coinbase confirmation height = 10
tip height                   = 50
min_confirmations            = 50

Its current confirmation count is:

confirmations = 50 - 10 + 1 = 41

The coinbase is still immature, but:

41 < 50

causes the confirmations < min_confirmations branch to execute first.

With trust_predicate returning true, the current implementation produces:

immature          = 0
trusted_pending   = coinbase_value
untrusted_pending = 0
confirmed         = 0

A minimal regression test for this case can assert:

assert_eq!(balance.immature, coinbase_value);
assert_eq!(balance.trusted_pending, Amount::ZERO);
assert_eq!(balance.untrusted_pending, Amount::ZERO);
assert_eq!(balance.confirmed, Amount::ZERO);

The immature assertion fails with the current condition ordering because the value is instead classified as trusted_pending.

The same issue occurs with trust_predicate = false, except the value is classified as untrusted_pending.


Expected behavior

An immature confirmed coinbase output should always be classified under Balance::immature, regardless of the caller-provided min_confirmations threshold.

Coinbase maturity should therefore be evaluated before applying the min_confirmations pending classification.

Conceptually, the ordering could be changed to:

if !txout.is_mature(self.tip.height) {
    immature += txout.txout.value;
} else if confirmations < min_confirmations {
    if trust_predicate(&spk_i, &txout) {
        trusted_pending += txout.txout.value;
    } else {
        untrusted_pending += txout.txout.value;
    }
} else if txout.is_confirmed_and_spendable(self.tip.height) {
    confirmed += txout.txout.value;
}

For non-coinbase outputs, is_mature() evaluates as mature, so they should continue through the existing min_confirmations classification path.


Build environment

  • BDK tag/commit: 456f9b7bbf510eefdf3e7a164a5d6a2be74ee800
  • OS+version: Windows
  • Rust/Cargo version: <!-- rustc --version / cargo --version -->
  • Rust/Cargo target: <!-- rustc -vV -->

Which backend(s) are relevant (if any)?

  • Electrum
  • Esplora
  • Bitcoin Core RPC
  • None / not backend-related (e.g. bdk_chain, bdk_core)
  • Other (please specify): ____

Is this blocking production use?

  • Yes
  • No

Project or organization (optional)

N/A

Additional context

Balance::immature is documented as containing coinbase outputs that have not yet matured. The current condition ordering allows the caller-defined min_confirmations threshold to take precedence over that maturity classification.

Current classification flow:

Confirmed output
       |
       v
confirmations < min_confirmations?
       |
      YES
       |
       +---- trust = true ----> trusted_pending
       |
       +---- trust = false ---> untrusted_pending
       |
       v
maturity check is skipped

Expected classification flow:

Confirmed output
       |
       v
Is output immature?
       |
      YES ----------> immature
       |
       NO
       v
confirmations < min_confirmations?
       |
      YES ----------> trusted/untrusted pending
       |
       NO
       v
confirmed/spendable

A focused regression test covering an immature coinbase where confirmations < min_confirmations should prevent this classification from regressing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    • Status
      No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions