bitcoindevkit / bitcoindevkit/bdk

Drop the "anchor may be a descendant" framing; introduce `Anchor::confirmation_height`

Open
#2,298 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
1.1k
Forks
483
Avg merge
20d 3h
Merged PRs (30d)
3

Description

### Summary

The `Anchor` trait documents that an anchor block may be a *descendant* of the block that actually confirmed a transaction. Nothing in the codebase produces such an anchor, and no chain source can. The framing costs us an "upper bound" name and a trail of pessimistic caveats through the public API for a case that does not occur.

Proposal: drop the framing, and introduce `Anchor::confirmation_height` alongside a deprecated `confirmation_height_upper_bound`.

### The framing

`crates/chain/src/tx_data_traits.rs`:

> If transaction A is anchored in block B, and block B is in the best chain, we can assume that transaction A is also confirmed in the best chain. **This does not necessarily mean that transaction A is confirmed in block B. It could also mean transaction A is confirmed in a parent block of B.**

and on the method:

> Get the upper bound of the chain data's confirmation height.
> The default definition gives a pessimistic answer.

### Why it does not hold in practice

Every anchor constructed in this repo uses the confirming block itself:

- **electrum** (`crates/electrum/src/bdk_electrum_client.rs:600`) builds the anchor only after `validate_merkle_proof(&txid, &header.merkle_root, &proof)` succeeds — the block provably contains the transaction.
- **esplora** (`crates/esplora/src/lib.rs:54`, and the `*_ext.rs` sites) uses `TxStatus { block_height, block_hash }`, which is the confirming block.
- **`TxPosInBlock`** (`crates/chain/src/tx_data_traits.rs:111`) carries `tx_pos`, the transaction's index *within that block* — structurally the confirming block. Both `From` impls (`BlockId`, `ConfirmationBlockTime`) pass `block_id` straight through.

Both in-tree `Anchor` impls define `confirmation_height_upper_bound()` as exactly `anchor_block().height`.

### What it costs

The pessimism propagates into unrelated public docs. `CanonicalTxOut::is_mature` and `is_confirmed_and_spendable` (`crates/chain/src/canonical.rs:148`, `:175`) both carry:

> Depending on the implementation of `confirmation_height_upper_bound` in `Anchor`, this method may return false-negatives. In other words, interpreted confirmation count may be less than the actual value.

So a caller reading the maturity API is told their coin may be reported immature when it isn't — a caveat that is unreachable with any anchor type that exists.

It also leaves ambiguity in ordering decisions. `BlockQueries::resolve_candidates` picks the lowest in-chain candidate among a transaction's anchors; under the current framing "several anchors in the best chain at once" is legal, so that choice is load-bearing. If anchors always name the confirming block, at most one can be in the best chain and the tie-break is defensive only.

### Proposed change

**1. Remove the "parent block" paragraph** from the `Anchor` trait docs, and the "pessimistic answer" note from the method. State instead that the anchor block is the block that confirmed the transaction.

**2. Add `confirmation_height`, deprecate `confirmation_height_upper_bound`.**

The delegation direction matters. The obvious version is wrong:

```rust
// WRONG: silently ignores existing overrides
fn confirmation_height(&self) -> u32 { self.anchor_block().height }

#[deprecated(note = "use `confirmation_height`")]
fn confirmation_height_upper_bound(&self) -> u32 { self.confirmation_height() }
```

A downstream impl that overrides `confirmation_height_upper_bound` today keeps compiling, but every internal call site moves to `confirmation_height` and stops seeing that override — a silent behaviour change, the worst outcome for a deprecation.

Default the *new* method to the old one instead, so existing overrides continue to be honoured:

```rust
fn confirmation_height(&self) -> u32 {
#[allow(deprecated)]
self.confirmation_height_upper_bound()
}

#[deprecated(since = "TBD", note = "use `confirmation_height`")]
fn confirmation_height_upper_bound(&self) -> u32 {
self.anchor_block().height
}
```

Then drop `confirmation_height_upper_bound` in a later release, at which point `confirmation_height` takes the `anchor_block().height` default.

**3. Rename `ChainPosition::confirmation_height_upper_bound`** (`crates/chain/src/chain_data.rs:78`) to match. This is an inherent method on a public type, so it needs its own `#[deprecated]` shim.

**4. Update the caveats** in `canonical.rs:148` and `:175` that only exist to describe the loose bound.

### Scope

Roughly 12 call sites across `canonical.rs`, `canonical_task.rs`, `canonical_view_task.rs`, `chain_data.rs`, `tx_graph.rs` (`insert_anchor`'s `txs_by_highest_conf_heights` bookkeeping), and `tx_data_traits.rs`. No behaviour change — both in-tree impls already return `anchor_block().height`.

### Open question

Is the "parent block of B" case something a chain source was ever expected to use — a compact-block or filter-based source that learns "confirmed at or before height N" without learning the exact block? If a design has that in mind, this should stay and the docs should name that source instead of leaving it hypothetical.

Contributor guide

Open the contributing guide

Research direction

Start with the Anchor trait in crates/chain/src/tx_data_traits.rs and ChainPosition in crates/chain/src/chain_data.rs, then trace the call sites listed in canonical.rs, canonical_task.rs, canonical_view_task.rs, tx_graph.rs, and tx_data_traits.rs. Update the public docs and compatibility shims, rename internal calls, and confirm that the existing in-tree anchor implementations and behavior remain unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
blockchain
Issue type
Refactor
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.