From cfab025b479b48ec73b0c19b40fabd0933d28aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 11 Sep 2026 01:46:53 +0000 Subject: [PATCH 1/2] docs(chain): state that an anchor names the confirming block The `Anchor` trait doc claimed an anchored transaction "could also mean transaction A is confirmed in a parent block of B". No chain source in this repo produces such an anchor: electrum only builds one after `validate_merkle_proof` succeeds, esplora uses `TxStatus`'s confirming block, and `TxPosInBlock` carries the transaction's index within that block. The framing only leaves ambiguity for a case that does not occur. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NND8BhEmYTYdLRv4dbUUro --- crates/chain/src/tx_data_traits.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 7ab8963f4c..d4948b35ee 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -2,10 +2,8 @@ use crate::{BlockId, ConfirmationBlockTime}; /// Trait that "anchors" blockchain data to a specific block of height and hash. /// -/// 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. +/// If transaction A is anchored in block B, then block B is the block that confirmed transaction +/// A. If block B is in the best chain, transaction A is confirmed in the best chain. /// /// Every [`Anchor`] implementation must contain a [`BlockId`] parameter, and must implement /// [`Ord`]. When implementing [`Ord`], the anchors' [`BlockId`]s should take precedence From 40cb3d1bbb8d53966ff36a72b1e3ba537c5bd743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 11 Sep 2026 01:51:10 +0000 Subject: [PATCH 2/2] refactor(chain)!: drop the bound framing from confirmation APIs An anchor names the block that confirmed the transaction, so its height is exact, not an upper bound. Rename `confirmation_height_upper_bound` to `confirmation_height` on both `Anchor` and `ChainPosition`. `ChainPosition::confirmations_lower_bound` was a lower bound only because it derives from that height: a higher height yields fewer confirmations, so an upper-bounded height yields a lower-bounded count. With the height exact the count is exact too, so it becomes `confirmations`. These are hard renames with no deprecated aliases. The next `bdk_chain` release is already breaking, so a downstream `Anchor` impl overriding an old name should fail to compile rather than be silently ignored. `ConfirmationBlockTime`'s override is removed as it was identical to the default. This drops the false-negative caveats from `CanonicalTxOut::is_mature` and `is_confirmed_and_spendable`, which only described the loose bound. No behaviour change. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NND8BhEmYTYdLRv4dbUUro --- crates/chain/src/canonical.rs | 22 +++++++--------------- crates/chain/src/canonical_task.rs | 2 +- crates/chain/src/chain_data.rs | 24 +++++++++++------------- crates/chain/src/tx_data_traits.rs | 15 +++++---------- crates/chain/src/tx_graph.rs | 8 ++------ 5 files changed, 26 insertions(+), 45 deletions(-) diff --git a/crates/chain/src/canonical.rs b/crates/chain/src/canonical.rs index c2aecb7561..ae1104c262 100644 --- a/crates/chain/src/canonical.rs +++ b/crates/chain/src/canonical.rs @@ -107,14 +107,12 @@ impl PartialOrd for CanonicalTxOut

{ impl CanonicalTxOut> { /// 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. - /// - /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound + /// A coinbase output is mature once [`COINBASE_MATURITY`] blocks (including the block that + /// confirmed it) have been mined up to and including `tip`. Non-coinbase outputs are always + /// mature. pub fn is_mature(&self, tip: u32) -> bool { if self.is_on_coinbase { - let conf_height = match self.pos.confirmation_height_upper_bound() { + let conf_height = match self.pos.confirmation_height() { Some(height) => height, None => { debug_assert!(false, "coinbase tx can never be unconfirmed"); @@ -133,18 +131,12 @@ impl CanonicalTxOut> { /// Whether the utxo is/was/will be spendable with chain `tip`. /// /// 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. - /// - /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound pub fn is_confirmed_and_spendable(&self, tip: u32) -> bool { if !self.is_mature(tip) { return false; } - let conf_height = match self.pos.confirmation_height_upper_bound() { + let conf_height = match self.pos.confirmation_height() { Some(height) => height, None => return false, }; @@ -156,7 +148,7 @@ impl CanonicalTxOut> { if let Some(spend_height) = self .spent_by .as_ref() - .and_then(|(pos, _)| pos.confirmation_height_upper_bound()) + .and_then(|(pos, _)| pos.confirmation_height()) { if spend_height <= tip { return false; @@ -449,7 +441,7 @@ impl CanonicalView { for (spk_i, txout) in self.filter_unspent_outpoints(outpoints) { match &txout.pos { ChainPosition::Confirmed { anchor, .. } => { - let confirmation_height = anchor.confirmation_height_upper_bound(); + let confirmation_height = anchor.confirmation_height(); let confirmations = self .tip .height diff --git a/crates/chain/src/canonical_task.rs b/crates/chain/src/canonical_task.rs index a7cb9230a2..e36df4d2a1 100644 --- a/crates/chain/src/canonical_task.rs +++ b/crates/chain/src/canonical_task.rs @@ -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(), )) } } diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index b03a968235..ddc5a11ec1 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -74,12 +74,10 @@ impl ChainPosition<&A> { } impl ChainPosition { - /// Determines the upper bound of the confirmation height. - pub fn confirmation_height_upper_bound(&self) -> Option { + /// Height of the block that confirmed this position, if it is confirmed. + pub fn confirmation_height(&self) -> Option { match self { - ChainPosition::Confirmed { anchor, .. } => { - Some(anchor.confirmation_height_upper_bound()) - } + ChainPosition::Confirmed { anchor, .. } => Some(anchor.confirmation_height()), ChainPosition::Unconfirmed { .. } => None, } } @@ -87,8 +85,8 @@ impl ChainPosition { /// Number of confirmations of this position, given `tip`. /// /// Returns `0` if unconfirmed, or if the confirmation height is above `tip`. - pub fn confirmations_lower_bound(&self, tip: u32) -> u32 { - let Some(height) = self.confirmation_height_upper_bound() else { + pub fn confirmations(&self, tip: u32) -> u32 { + let Some(height) = self.confirmation_height() else { return 0; }; @@ -352,7 +350,7 @@ mod test { } #[test] - fn test_confirmations_lower_bound() { + fn test_confirmations() { let confirmed_at = |height: u32| ChainPosition::Confirmed { anchor: ConfirmationBlockTime { confirmation_time: 0, @@ -364,15 +362,15 @@ mod test { transitively: None, }; - assert_eq!(confirmed_at(100).confirmations_lower_bound(100), 1); - assert_eq!(confirmed_at(99).confirmations_lower_bound(100), 2); - assert_eq!(confirmed_at(90).confirmations_lower_bound(100), 11); - assert_eq!(confirmed_at(101).confirmations_lower_bound(100), 0); + assert_eq!(confirmed_at(100).confirmations(100), 1); + assert_eq!(confirmed_at(99).confirmations(100), 2); + assert_eq!(confirmed_at(90).confirmations(100), 11); + assert_eq!(confirmed_at(101).confirmations(100), 0); let unconfirmed = ChainPosition::::Unconfirmed { first_seen: Some(1), last_seen: Some(2), }; - assert_eq!(unconfirmed.confirmations_lower_bound(100), 0); + assert_eq!(unconfirmed.confirmations(100), 0); } } diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index d4948b35ee..9989374341 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -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 Anchor for &A { ::anchor_block(self) } - fn confirmation_height_upper_bound(&self) -> u32 { - ::confirmation_height_upper_bound(self) + fn confirmation_height(&self) -> u32 { + ::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`]. diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index df0fb01d72..44a2f81af5 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -766,15 +766,11 @@ impl TxGraph { // `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;