From: 志宇 Date: Fri, 11 Sep 2026 01:51:10 +0000 (+0000) Subject: refactor(chain)!: rename Anchor::confirmation_height_upper_bound X-Git-Url: http://internal-gitweb-vhost/-client-interface/src/example_cli/parse/struct.CodeLengthError.html?a=commitdiff_plain;h=dadeb37fd94ca7ded7adee2d2bf69047135f6d23;p=bdk refactor(chain)!: rename Anchor::confirmation_height_upper_bound 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 Claude-Session: https://claude.ai/code/session_01NND8BhEmYTYdLRv4dbUUro Co-Authored-By: Claude Opus 5.5 --- diff --git a/crates/chain/src/canonical.rs b/crates/chain/src/canonical.rs index 20603c8d..98c1a4ca 100644 --- a/crates/chain/src/canonical.rs +++ b/crates/chain/src/canonical.rs @@ -131,11 +131,17 @@ 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. + /// 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 CanonicalTxOut> { /// /// 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; diff --git a/crates/chain/src/canonical_task.rs b/crates/chain/src/canonical_task.rs index a7cb9230..e36df4d2 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 b03a9682..2bfe5913 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -75,18 +75,25 @@ impl ChainPosition<&A> { impl ChainPosition { /// 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 { 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; diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index d4948b35..99893743 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 df0fb01d..44a2f81a 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; diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index 07760a8a..107efeeb 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -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| { + 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); +}