]> Untitled Git - bdk/commitdiff
refactor(chain)!: rename Anchor::confirmation_height_upper_bound
author志宇 <hello@evanlinjin.me>
Fri, 11 Sep 2026 01:51:10 +0000 (01:51 +0000)
committer志宇 <hello@evanlinjin.me>
Tue, 13 Oct 2026 01:33:18 +0000 (01:33 +0000)
An anchor names the block that confirmed the transaction, so its height
is exact, not an upper bound. Rename
`Anchor::confirmation_height_upper_bound` to
`Anchor::confirmation_height`.

This is a hard rename with no deprecated alias. The next `bdk_chain`
release is already breaking, so a downstream `Anchor` impl overriding
the old name should fail to compile rather than be silently ignored.
`ConfirmationBlockTime`'s override is removed as it was identical to the
default.

`ChainPosition::confirmation_height_upper_bound` and
`ChainPosition::confirmations_lower_bound` keep their names. A
transaction without its own anchor can be confirmed transitively during
canonicalization, in which case the position carries a descendant's
anchor and the height really is an upper bound. Their docs, and the
false-negative caveats on `CanonicalTxOut::is_mature` and
`is_confirmed_and_spendable`, now attribute the bound to transitive
confirmation instead of to the `Anchor` implementation. A test covers
the transitive case.

No behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NND8BhEmYTYdLRv4dbUUro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crates/chain/src/canonical.rs
crates/chain/src/canonical_task.rs
crates/chain/src/chain_data.rs
crates/chain/src/tx_data_traits.rs
crates/chain/src/tx_graph.rs
crates/chain/tests/test_tx_graph_conflicts.rs

index 20603c8dd83572f0b4ebb9e0c4b9eac32b19159a..98c1a4ca3a96c8a9e9ec2657605cd4ae3013d3bb 100644 (file)
@@ -131,11 +131,17 @@ impl<P: Ord> PartialOrd for CanonicalTxOut<P> {
 impl<A: Anchor> CanonicalTxOut<ChainPosition<A>> {
     /// Whether the `txout` is considered mature.
     ///
-    /// 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.
+    /// A coinbase output is mature once a transaction spending it can be included in the block
+    /// after `tip`. Consensus requires the spending block to be at least [`COINBASE_MATURITY`]
+    /// blocks above the confirming block, so the output is mature once `tip` is at least
+    /// `COINBASE_MATURITY - 1` blocks above the confirming block. Non-coinbase outputs are always
+    /// mature.
     ///
-    /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
+    /// If the position is confirmed [`transitively`](ChainPosition::Confirmed::transitively), the
+    /// confirmation height may be an upper bound (see [`confirmation_height_upper_bound`]), so
+    /// this method may return false-negatives.
+    ///
+    /// [`confirmation_height_upper_bound`]: ChainPosition::confirmation_height_upper_bound
     pub fn is_mature(&self, tip: u32) -> bool {
         if self.is_on_coinbase {
             let conf_height = match self.pos.confirmation_height_upper_bound() {
@@ -158,11 +164,11 @@ impl<A: Anchor> CanonicalTxOut<ChainPosition<A>> {
     ///
     /// This method does not take into account the lock time.
     ///
-    /// 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.
+    /// If the position is confirmed [`transitively`](ChainPosition::Confirmed::transitively), the
+    /// confirmation height may be an upper bound (see [`confirmation_height_upper_bound`]), so
+    /// this method may return false-negatives.
     ///
-    /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
+    /// [`confirmation_height_upper_bound`]: ChainPosition::confirmation_height_upper_bound
     pub fn is_confirmed_and_spendable(&self, tip: u32) -> bool {
         if !self.is_mature(tip) {
             return false;
index a7cb9230a27df1e85799e311cfed644985ca4feb..e36df4d2a17e25d5d67e3af5d43c2d97a5a9aec2 100644 (file)
@@ -178,7 +178,7 @@ impl<'g, A: Anchor> ChainQuery for CanonicalTask<'g, A> {
                                     .expect(
                                         "tx taken from `unprocessed_anchored_txs` so it must have at least one anchor",
                                     )
-                                    .confirmation_height_upper_bound(),
+                                    .confirmation_height(),
                             ))
                         }
                     }
index b03a9682354a3e7d92c8875a6354ee59942c4627..2bfe591343e400451aa8203d8743a2732707492e 100644 (file)
@@ -75,18 +75,25 @@ impl<A: Clone> ChainPosition<&A> {
 
 impl<A: Anchor> ChainPosition<A> {
     /// Determines the upper bound of the confirmation height.
+    ///
+    /// This is the height of the anchor block. It is exact for a direct anchor. If the position is
+    /// confirmed [`transitively`](ChainPosition::Confirmed::transitively), the anchor belongs to a
+    /// descendant, so the chain data may be confirmed in an earlier block.
     pub fn confirmation_height_upper_bound(&self) -> Option<u32> {
         match self {
-            ChainPosition::Confirmed { anchor, .. } => {
-                Some(anchor.confirmation_height_upper_bound())
-            }
+            ChainPosition::Confirmed { anchor, .. } => Some(anchor.confirmation_height()),
             ChainPosition::Unconfirmed { .. } => None,
         }
     }
 
-    /// Number of confirmations of this position, given `tip`.
+    /// Lower bound of the number of confirmations of this position, given `tip`.
+    ///
+    /// This is derived from [`confirmation_height_upper_bound`], so it is exact unless the position
+    /// is confirmed [`transitively`](ChainPosition::Confirmed::transitively).
     ///
     /// Returns `0` if unconfirmed, or if the confirmation height is above `tip`.
+    ///
+    /// [`confirmation_height_upper_bound`]: Self::confirmation_height_upper_bound
     pub fn confirmations_lower_bound(&self, tip: u32) -> u32 {
         let Some(height) = self.confirmation_height_upper_bound() else {
             return 0;
index d4948b35eeaf16f3f9d9a313fa0b8c0ee2586761..998937434122e302a03e2d1e492a54caa858851a 100644 (file)
@@ -66,11 +66,10 @@ pub trait Anchor: core::fmt::Debug + Clone + Eq + PartialOrd + Ord + core::hash:
     /// Returns the [`BlockId`] that the associated blockchain data is "anchored" in.
     fn anchor_block(&self) -> BlockId;
 
-    /// Get the upper bound of the chain data's confirmation height.
+    /// Get the height of the block that confirmed the associated chain data.
     ///
-    /// The default definition gives a pessimistic answer. This can be overridden by the `Anchor`
-    /// implementation for a more accurate value.
-    fn confirmation_height_upper_bound(&self) -> u32 {
+    /// This is the height of [`anchor_block`](Anchor::anchor_block).
+    fn confirmation_height(&self) -> u32 {
         self.anchor_block().height
     }
 }
@@ -80,8 +79,8 @@ impl<A: Anchor> Anchor for &A {
         <A as Anchor>::anchor_block(self)
     }
 
-    fn confirmation_height_upper_bound(&self) -> u32 {
-        <A as Anchor>::confirmation_height_upper_bound(self)
+    fn confirmation_height(&self) -> u32 {
+        <A as Anchor>::confirmation_height(self)
     }
 }
 
@@ -95,10 +94,6 @@ impl Anchor for ConfirmationBlockTime {
     fn anchor_block(&self) -> BlockId {
         self.block_id
     }
-
-    fn confirmation_height_upper_bound(&self) -> u32 {
-        self.block_id.height
-    }
 }
 
 /// Set of parameters sufficient to construct an [`Anchor`].
index df0fb01d721f22eaed3d5778d3a8bbadfe3c131f..44a2f81af5f4d420e15fb6c5f11af0534bd64f63 100644 (file)
@@ -766,15 +766,11 @@ impl<A: Anchor> TxGraph<A> {
         // `txs_by_highest_conf_heights`.
         // We want to remove `(old_top_h?, txid)` and insert `(new_top_h?, txid)`.
         let mut old_top_h = None;
-        let mut new_top_h = anchor.confirmation_height_upper_bound();
+        let mut new_top_h = anchor.confirmation_height();
 
         let is_changed = match self.anchors.entry(txid) {
             hash_map::Entry::Occupied(mut e) => {
-                old_top_h = e
-                    .get()
-                    .iter()
-                    .last()
-                    .map(Anchor::confirmation_height_upper_bound);
+                old_top_h = e.get().iter().last().map(Anchor::confirmation_height);
                 if let Some(old_top_h) = old_top_h {
                     if old_top_h > new_top_h {
                         new_top_h = old_top_h;
index 07760a8a1db0ec9bb07313c13e53c5d1b0d35f1e..107efeeb638d9e07e3dca515ed29da2bdc1dfd8e 100644 (file)
@@ -3,7 +3,7 @@
 #[macro_use]
 mod common;
 
-use bdk_chain::{local_chain::LocalChain, Balance, BlockId};
+use bdk_chain::{local_chain::LocalChain, Balance, BlockId, ChainPosition, TxGraph};
 use bdk_testenv::{block_id, hash, local_chain};
 use bitcoin::{Amount, BlockHash, OutPoint};
 use common::*;
@@ -1006,3 +1006,72 @@ fn test_tx_conflict_handling() {
         );
     }
 }
+
+/// A tx without its own anchor borrows the anchor of a confirmed descendant, so its confirmation
+/// height is only an upper bound until its own anchor is known.
+#[test]
+fn transitively_confirmed_tx_reports_descendant_anchor_height() {
+    let chain = local_chain![
+        (0, hash!("genesis")),
+        (1, hash!("parent_block")),
+        (5, hash!("child_block"))
+    ];
+    let tip = chain.tip().block_id();
+    let parent_anchor = block_id!(1, "parent_block");
+    let child_anchor = block_id!(5, "child_block");
+
+    let templates = [
+        TxTemplate {
+            tx_name: "parent",
+            inputs: &[TxInTemplate::Bogus],
+            outputs: &[TxOutTemplate::new(100_000, Some(0))],
+            // Seen in the mempool, but its confirmation has not been fetched yet.
+            last_seen: Some(1),
+            ..Default::default()
+        },
+        TxTemplate {
+            tx_name: "child",
+            inputs: &[TxInTemplate::PrevTx("parent", 0)],
+            outputs: &[TxOutTemplate::new(90_000, Some(0))],
+            anchors: &[child_anchor],
+            ..Default::default()
+        },
+    ];
+    let mut env = init_graph(&templates);
+    let parent_txid = env.txid_to_name["parent"];
+    let child_txid = env.txid_to_name["child"];
+
+    let parent_position = |tx_graph: &TxGraph<BlockId>| {
+        chain
+            .canonical_view(tx_graph, tip, Default::default())
+            .txs()
+            .find(|tx| tx.txid == parent_txid)
+            .expect("parent must be canonical")
+            .pos
+    };
+
+    // Really confirmed in block 1, but the position can only report the child's block 5.
+    let pos = parent_position(&env.tx_graph);
+    assert_eq!(
+        pos,
+        ChainPosition::Confirmed {
+            anchor: child_anchor,
+            transitively: Some(child_txid),
+        }
+    );
+    assert_eq!(pos.confirmation_height_upper_bound(), Some(5));
+    assert_eq!(pos.confirmations_lower_bound(tip.height), 1);
+
+    // Once the parent's own anchor is known, the position becomes exact.
+    let _ = env.tx_graph.insert_anchor(parent_txid, parent_anchor);
+    let pos = parent_position(&env.tx_graph);
+    assert_eq!(
+        pos,
+        ChainPosition::Confirmed {
+            anchor: parent_anchor,
+            transitively: None,
+        }
+    );
+    assert_eq!(pos.confirmation_height_upper_bound(), Some(1));
+    assert_eq!(pos.confirmations_lower_bound(tip.height), 5);
+}