From 1e4ba9d11ec5b379e1fd9d2879937ceb6d6bd270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 26 Oct 2026 01:55:21 +0000 Subject: [PATCH 1/2] fix(chain): make `LocalChain` canonicalization generic over `D` `is_block_in_chain`, `chain_tip`, `canonicalize` and `canonical_view` only need block hashes, which any checkpoint data provides, but they were in `impl LocalChain`, so a `LocalChain
` could not canonicalize a `TxGraph`. Make that impl block generic over `D`. Co-Authored-By: Claude Opus 5.5 --- crates/chain/src/local_chain.rs | 7 ++++-- .../chain/tests/test_canonical_view_task.rs | 6 ++--- crates/chain/tests/test_local_chain.rs | 22 +++++++++++++++++++ crates/chain/tests/test_tx_graph_conflicts.rs | 2 +- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 901f279172..0c939bd6c3 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -84,8 +84,8 @@ impl PartialEq for LocalChain { } } -// Methods for `LocalChain` -impl LocalChain { +// Methods for any `D` +impl LocalChain { /// Check if a block is in the chain. /// /// # Arguments @@ -167,7 +167,10 @@ impl LocalChain { let canonical_txs = self.canonicalize(tx_graph.canonical_task(tip, params)); self.canonicalize(canonical_txs.view_task(tx_graph)) } +} +// Methods for `LocalChain` +impl LocalChain { /// Update the chain with a given [`Header`] at `height` which you claim is connected to a /// existing block in the chain. /// diff --git a/crates/chain/tests/test_canonical_view_task.rs b/crates/chain/tests/test_canonical_view_task.rs index 397819f4fc..1fc96be3a7 100644 --- a/crates/chain/tests/test_canonical_view_task.rs +++ b/crates/chain/tests/test_canonical_view_task.rs @@ -2,9 +2,9 @@ mod common; -use bdk_chain::{CanonicalReason, ChainPosition}; +use bdk_chain::{local_chain::LocalChain, CanonicalReason, ChainPosition}; use bdk_testenv::{block_id, hash, local_chain}; -use bitcoin::Txid; +use bitcoin::{BlockHash, Txid}; use common::*; use std::collections::HashSet; @@ -13,7 +13,7 @@ fn test_assumed_canonical_scenarios() { // scenario: "txC spends txB; txB spends txA; txB is anchored; txC is assumed canonical" // create a local chain - let local_chain = local_chain![ + let local_chain: LocalChain = local_chain![ (0, hash!("genesis")), (1, hash!("block1")), (2, hash!("block2")), diff --git a/crates/chain/tests/test_local_chain.rs b/crates/chain/tests/test_local_chain.rs index e6758559d9..43a35ba216 100644 --- a/crates/chain/tests/test_local_chain.rs +++ b/crates/chain/tests/test_local_chain.rs @@ -406,6 +406,28 @@ fn header(prev_blockhash: BlockHash) -> Header { } } +/// A `LocalChain
` answers chain queries and canonicalizes a `TxGraph`. +#[test] +fn local_chain_header_is_block_in_chain() { + let genesis = header(hash!("_")); + let block_1 = header(genesis.block_hash()); + let (mut chain, _) = LocalChain::from_genesis(genesis); + chain.insert_block(1, block_1).expect("must connect"); + + let tip = chain.chain_tip(); + assert_eq!(tip, BlockId::from((1, block_1.block_hash()))); + let genesis_id = BlockId::from((0, genesis.block_hash())); + assert_eq!(chain.is_block_in_chain(genesis_id, tip), Some(true)); + assert_eq!( + chain.is_block_in_chain(BlockId::from((0, hash!("other"))), tip), + Some(false) + ); + + let graph = bdk_chain::TxGraph::::default(); + let view = chain.canonical_view(&graph, tip, Default::default()); + assert_eq!(view.txs().count(), 0); +} + #[test] fn local_chain_insert_header() { // Create consecutive headers of height `n`, where the genesis header is height 0. diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index 2f6f1fd6bb..a9f892a07a 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -1078,7 +1078,7 @@ fn test_tx_conflict_handling() { /// 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![ + let chain: LocalChain = local_chain![ (0, hash!("genesis")), (1, hash!("parent_block")), (5, hash!("child_block")) From 0b280d9703db5f68f5a1c3cc87ea7101e7aba169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 26 Oct 2026 01:55:24 +0000 Subject: [PATCH 2/2] fix(chain): implement request builder exts for any checkpoint data `SyncRequestBuilderExt` and `FullScanRequestBuilderExt` never touch the checkpoint data, but were only implemented for `D = BlockHash`. Co-Authored-By: Claude Opus 5.5 --- crates/chain/src/indexer/keychain_txout.rs | 8 +++-- .../chain/tests/test_keychain_txout_index.rs | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/crates/chain/src/indexer/keychain_txout.rs b/crates/chain/src/indexer/keychain_txout.rs index 4717cffdc8..92519982d8 100644 --- a/crates/chain/src/indexer/keychain_txout.rs +++ b/crates/chain/src/indexer/keychain_txout.rs @@ -1129,7 +1129,9 @@ pub trait SyncRequestBuilderExt { fn unused_spks_from_indexer(self, indexer: &KeychainTxOutIndex) -> Self; } -impl SyncRequestBuilderExt for SyncRequestBuilder<(K, u32)> { +impl SyncRequestBuilderExt + for SyncRequestBuilder<(K, u32), D> +{ fn revealed_spks_from_indexer(self, indexer: &KeychainTxOutIndex, spk_range: R) -> Self where R: core::ops::RangeBounds, @@ -1148,7 +1150,9 @@ pub trait FullScanRequestBuilderExt { fn spks_from_indexer(self, indexer: &KeychainTxOutIndex) -> Self; } -impl FullScanRequestBuilderExt for FullScanRequestBuilder { +impl FullScanRequestBuilderExt + for FullScanRequestBuilder +{ fn spks_from_indexer(mut self, indexer: &KeychainTxOutIndex) -> Self { for (keychain, spks) in indexer.all_unbounded_spk_iters() { let last_revealed = indexer.last_revealed_index(keychain.clone()); diff --git a/crates/chain/tests/test_keychain_txout_index.rs b/crates/chain/tests/test_keychain_txout_index.rs index 3dea4544b4..03a058b955 100644 --- a/crates/chain/tests/test_keychain_txout_index.rs +++ b/crates/chain/tests/test_keychain_txout_index.rs @@ -761,3 +761,38 @@ fn overlapping_descriptor_does_not_panic_in_release() { let _ = index.insert_descriptor(1, single); assert!(index.reveal_next_spk(1).is_none()); } + +/// The request builder extensions work for any checkpoint data, not only `BlockHash`. +#[test] +fn request_builder_ext_works_with_any_checkpoint_data() { + use bdk_chain::{ + bitcoin::{block::Header, constants::genesis_block, Network}, + indexer::keychain_txout::{FullScanRequestBuilderExt, SyncRequestBuilderExt}, + spk_client::{FullScanRequest, SyncRequest}, + CheckPoint, + }; + + let mut indexer = init_txout_index( + parse_descriptor(DESCRIPTORS[0]), + parse_descriptor(DESCRIPTORS[1]), + 0, + false, + ); + let _ = indexer.reveal_to_target(TestKeychain::External, 2); + let tip = CheckPoint::
::new(0, genesis_block(Network::Regtest).header); + + let sync_request = SyncRequest::builder_at(0) + .chain_tip(tip.clone()) + .revealed_spks_from_indexer(&indexer, ..) + .build(); + assert_eq!(sync_request.spks().len(), 3); + + let full_scan_request = FullScanRequest::builder_at(0) + .chain_tip(tip) + .spks_from_indexer(&indexer) + .build(); + assert_eq!( + full_scan_request.last_revealed(&TestKeychain::External), + Some(2) + ); +}