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:
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)?
Is this blocking production use?
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.
Describe the bug
CanonicalView::balancecan misclassify an immature confirmed coinbase output astrusted_pendingoruntrusted_pendingwhen its current confirmation count is below the caller-providedmin_confirmations.The issue is in
crates/chain/src/canonical.rs, inside theChainPosition::Confirmedbranch ofCanonicalView::balance.The current logic checks
confirmations < min_confirmationsbefore checking whether the output is mature:This means that when an immature coinbase satisfies:
the first branch is taken and the maturity check is never reached.
As a result, the immature coinbase value is placed into
trusted_pendingoruntrusted_pendinginstead ofBalance::immature.This can also affect the semantics of
Balance::trusted_spendable(), sincetrusted_pendingcontributes to that value while an immature coinbase output is not yet spendable.To Reproduce
Consider a confirmed coinbase output with:
Its current confirmation count is:
The coinbase is still immature, but:
causes the
confirmations < min_confirmationsbranch to execute first.With
trust_predicatereturningtrue, the current implementation produces:A minimal regression test for this case can assert:
The
immatureassertion fails with the current condition ordering because the value is instead classified astrusted_pending.The same issue occurs with
trust_predicate = false, except the value is classified asuntrusted_pending.Expected behavior
An immature confirmed coinbase output should always be classified under
Balance::immature, regardless of the caller-providedmin_confirmationsthreshold.Coinbase maturity should therefore be evaluated before applying the
min_confirmationspending classification.Conceptually, the ordering could be changed to:
For non-coinbase outputs,
is_mature()evaluates as mature, so they should continue through the existingmin_confirmationsclassification path.Build environment
456f9b7bbf510eefdf3e7a164a5d6a2be74ee800<!-- rustc --version / cargo --version --><!-- rustc -vV -->Which backend(s) are relevant (if any)?
bdk_chain,bdk_core)____Is this blocking production use?
Project or organization (optional)
N/A
Additional context
Balance::immatureis documented as containing coinbase outputs that have not yet matured. The current condition ordering allows the caller-definedmin_confirmationsthreshold to take precedence over that maturity classification.Current classification flow:
Expected classification flow:
A focused regression test covering an immature coinbase where
confirmations < min_confirmationsshould prevent this classification from regressing.