Skip to content

fix(chain): populate direct_anchors for non-transitive anchor reasons - #2256

Closed
harry9879 wants to merge 1 commit into
bitcoindevkit:masterfrom
harry9879:fix/canonical-view-direct-anchor-descendant
Closed

fix(chain): populate direct_anchors for non-transitive anchor reasons#2256
harry9879 wants to merge 1 commit into
bitcoindevkit:masterfrom
harry9879:fix/canonical-view-direct-anchor-descendant

Conversation

@harry9879

@harry9879 harry9879 commented Aug 6, 2026

Copy link
Copy Markdown

Summary

CanonicalViewTask::new only queues transactions with a transitive or assumed CanonicalReason for the direct-anchor lookup performed via ChainQuery. A non-transitive Anchor { descendant: None, .. } reason is skipped, even though its anchor was already chain-verified when the transaction was first canonicalized — so it never ends up in direct_anchors.

This breaks resolution for an assume_canonical transaction whose confirmation comes from a directly anchored descendant. finish() handles this case (an assumed root/ancestor with no anchor of its own) by walking its descendants and looking for one present in direct_anchors:

None => TxDescendants::new_exclude_root(self.tx_graph, *txid, |_, desc_txid| {
    self.canonical_txs.contains_key(&desc_txid).then_some(desc_txid)
})
.find_map(|desc_txid| {
    self.direct_anchors.get(&desc_txid).map(|anchor| ChainPosition::Confirmed { anchor, transitively: Some(desc_txid) })
})
.unwrap_or(ChainPosition::Unconfirmed { .. }),

If that descendant only has a non-transitive Anchor reason (the ordinary case for a genuinely confirmed descendant that isn't itself an ancestor of the assumed root), the lookup misses and the assumed transaction incorrectly resolves to Unconfirmed instead of Confirmed.

Note this is distinct from the already-covered case where an ancestor of the assumed root has its own anchor — in that case "assumed" propagation relabels the ancestor's reason to Assumed { descendant: Some(root) } (transitive), which is queued and does get into direct_anchors. The gap is specifically for descendants that keep their own ordinary Anchor reason.

Fix

Seed direct_anchors immediately in CanonicalViewTask::new for non-transitive Anchor reasons, since their anchor is already verified and doesn't need a redundant chain query — this makes it visible to the descendant search in finish().

Impact

A confirmed, spendable UTXO could be misreported as unconfirmed/untrusted when using CanonicalizationParams::assume_canonical, causing wrong balance categorization and false negatives on confirmation/maturity checks. Direction is "too conservative" (no double-spend or fund-loss risk). bdk_wallet doesn't currently pass assume_canonical in its default flow, so this isn't reachable through typical wallet usage today, but it's a reachable bug for any downstream consumer using that public API directly.

CanonicalViewTask::new only queued transactions with a transitive or
assumed CanonicalReason for the direct-anchor lookup, so a
non-transitive Anchor { descendant: None, .. } reason never landed in
direct_anchors even though its anchor was already chain-verified.

This broke resolution for an assume_canonical transaction whose
confirmation comes from a directly anchored descendant (rather than
from being an ancestor of the assumed-canonical root, which is
covered by the existing transitive-Anchor propagation): finish()
walks the descendant chain looking for an entry in direct_anchors and
found nothing, so the transaction was reported ChainPosition::Unconfirmed
instead of Confirmed.

Seed direct_anchors immediately for non-transitive Anchor reasons
since their anchor is already verified, making it visible to that
descendant search without needing a redundant chain query.

Adds a regression test covering an assumed-canonical root with a
directly anchored descendant.
@harry9879 harry9879 closed this Aug 9, 2026
@github-project-automation github-project-automation Bot moved this to Done in BDK Chain Aug 9, 2026
@harry9879
harry9879 deleted the fix/canonical-view-direct-anchor-descendant branch August 9, 2026 03:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant