diff --git a/src/chain/cbf.rs b/src/chain/cbf.rs index bd8f36691..f7c1b109a 100644 --- a/src/chain/cbf.rs +++ b/src/chain/cbf.rs @@ -448,6 +448,7 @@ impl CbfChainSource { reorganized.len(), accepted.len(), ); + *state.latest_tip.lock().unwrap() = None; // No height reset needed: skip heights are derived from // BDK's checkpoint (on-chain) and LDK's best block @@ -609,19 +610,18 @@ impl CbfChainSource { }, }; - // Build chain checkpoint extending from the wallet's current tip. + // Build chain checkpoint extending from the wallet's current tip, + // using `insert` (not `push`) so that reorgs are handled correctly. + // `insert` detects conflicting hashes and purges stale blocks, + // matching bdk-kyoto's approach in `UpdateBuilder::apply_chain_event`. let mut cp = onchain_wallet.latest_checkpoint(); for (height, header) in sync_update.recent_history() { - if *height > cp.height() { - let block_id = BlockId { height: *height, hash: header.block_hash() }; - cp = cp.push(block_id).unwrap_or_else(|old| old); - } + let block_id = BlockId { height: *height, hash: header.block_hash() }; + cp = cp.insert(block_id); } let tip = sync_update.tip(); - if tip.height > cp.height() { - let tip_block_id = BlockId { height: tip.height, hash: tip.hash }; - cp = cp.push(tip_block_id).unwrap_or_else(|old| old); - } + let tip_block_id = BlockId { height: tip.height, hash: tip.hash }; + cp = cp.insert(tip_block_id); let update = Update { last_active_indices: BTreeMap::new(), tx_update, chain: Some(cp) }; @@ -1396,4 +1396,71 @@ mod tests { } } } + + /// Test that checkpoint building from `recent_history` handles reorgs. + /// + /// Scenario: wallet synced to height 103. A 3-block reorg replaces blocks + /// 101-103 with new ones (same tip height). `recent_history` returns + /// {94..=103} (last 10 blocks ending at tip) with new hashes at 101-103. + /// + /// The checkpoint must reflect the reorged chain: new hashes at 101-103, + /// pre-reorg blocks at ≤100 preserved. + #[test] + fn checkpoint_building_handles_reorg() { + use bdk_chain::local_chain::LocalChain; + use bdk_chain::{BlockId, CheckPoint}; + use bitcoin::BlockHash; + use std::collections::BTreeMap; + + fn hash(seed: u32) -> BlockHash { + use bitcoin::hashes::{sha256d, Hash, HashEngine}; + let mut engine = sha256d::Hash::engine(); + engine.input(&seed.to_le_bytes()); + BlockHash::from_raw_hash(sha256d::Hash::from_engine(engine)) + } + + let genesis = BlockId { height: 0, hash: hash(0) }; + + // Wallet checkpoint: 0 → 100 → 101 → 102 → 103 + let wallet_cp = CheckPoint::from_block_ids([ + genesis, + BlockId { height: 100, hash: hash(100) }, + BlockId { height: 101, hash: hash(101) }, + BlockId { height: 102, hash: hash(102) }, + BlockId { height: 103, hash: hash(103) }, + ]) + .unwrap(); + + // recent_history after reorg: 94-103, heights 101-103 have NEW hashes. + let recent_history: BTreeMap = (94..=103) + .map(|h| { + let seed = if (101..=103).contains(&h) { h + 1000 } else { h }; + (h, hash(seed)) + }) + .collect(); + + // Build checkpoint using the same logic as sync_onchain_wallet. + let mut cp = wallet_cp; + for (height, block_hash) in &recent_history { + let block_id = BlockId { height: *height, hash: *block_hash }; + cp = cp.insert(block_id); + } + + // Reorged blocks must have the NEW hashes. + assert_eq!(cp.height(), 103); + assert_eq!( + cp.get(101).expect("height 101 must exist").hash(), + hash(1101), + "block 101 must have the reorged hash" + ); + assert_eq!(cp.get(102).expect("height 102 must exist").hash(), hash(1102)); + assert_eq!(cp.get(103).expect("height 103 must exist").hash(), hash(1103)); + + // Pre-reorg blocks are preserved. + assert_eq!(cp.get(100).expect("height 100 must exist").hash(), hash(100)); + + // The checkpoint must connect cleanly to a LocalChain. + let (mut chain, _) = LocalChain::from_genesis_hash(genesis.hash); + chain.apply_update(cp).expect("checkpoint must connect to chain"); + } } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 6a3c3c70f..6ef3a2eef 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -23,9 +23,9 @@ use common::{ expect_payment_successful_event, expect_splice_pending_event, generate_blocks_and_wait, generate_listening_addresses, open_channel, open_channel_push_amt, open_channel_with_all, premine_and_distribute_funds, premine_blocks, prepare_rbf, random_chain_source, random_config, - random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, setup_node, - setup_two_nodes, skip_if_cbf, splice_in_with_all, wait_for_cbf_sync, wait_for_tx, - TestChainSource, TestStoreType, TestSyncStore, + setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, skip_if_cbf, + splice_in_with_all, wait_for_cbf_sync, wait_for_tx, TestChainSource, TestStoreType, + TestSyncStore, }; use electrsd::corepc_node::Node as BitcoinD; use electrsd::ElectrsD;