Skip to content

Commit afac435

Browse files
randomloginclaude
authored andcommitted
fix(cbf): use CheckPoint::insert for reorg-aware wallet sync
`push` only appends above the tip, so when `recent_history` contained blocks at or below the wallet's current checkpoint height after a reorg, the stale hashes on the wallet checkpoint were never replaced. Switch to `CheckPoint::insert`, which detects conflicting hashes and purges stale blocks, matching bdk-kyoto's `UpdateBuilder::apply_chain_event`. Also clear `latest_tip` on `BlockHeaderChanges::Reorganized` so cached tip state does not point at an abandoned chain. Update the `checkpoint_building_handles_reorg` unit test (added in c1844b3) to exercise the fixed behaviour: a reorg where the new tip is at the same height as the wallet's checkpoint must still result in the reorged hashes winning. Disclosure: drafted with assistance from Claude Code. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 82d7852 commit afac435

2 files changed

Lines changed: 20 additions & 27 deletions

File tree

src/chain/cbf.rs

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ impl CbfChainSource {
448448
reorganized.len(),
449449
accepted.len(),
450450
);
451+
*state.latest_tip.lock().unwrap() = None;
451452

452453
// No height reset needed: skip heights are derived from
453454
// BDK's checkpoint (on-chain) and LDK's best block
@@ -609,19 +610,18 @@ impl CbfChainSource {
609610
},
610611
};
611612

612-
// Build chain checkpoint extending from the wallet's current tip.
613+
// Build chain checkpoint extending from the wallet's current tip,
614+
// using `insert` (not `push`) so that reorgs are handled correctly.
615+
// `insert` detects conflicting hashes and purges stale blocks,
616+
// matching bdk-kyoto's approach in `UpdateBuilder::apply_chain_event`.
613617
let mut cp = onchain_wallet.latest_checkpoint();
614618
for (height, header) in sync_update.recent_history() {
615-
if *height > cp.height() {
616-
let block_id = BlockId { height: *height, hash: header.block_hash() };
617-
cp = cp.push(block_id).unwrap_or_else(|old| old);
618-
}
619+
let block_id = BlockId { height: *height, hash: header.block_hash() };
620+
cp = cp.insert(block_id);
619621
}
620622
let tip = sync_update.tip();
621-
if tip.height > cp.height() {
622-
let tip_block_id = BlockId { height: tip.height, hash: tip.hash };
623-
cp = cp.push(tip_block_id).unwrap_or_else(|old| old);
624-
}
623+
let tip_block_id = BlockId { height: tip.height, hash: tip.hash };
624+
cp = cp.insert(tip_block_id);
625625

626626
let update =
627627
Update { last_active_indices: BTreeMap::new(), tx_update, chain: Some(cp) };
@@ -1399,11 +1399,11 @@ mod tests {
13991399
/// Test that checkpoint building from `recent_history` handles reorgs.
14001400
///
14011401
/// Scenario: wallet synced to height 103. A 3-block reorg replaces blocks
1402-
/// 101-103 with new ones, and `recent_history` returns {97..=106} with
1403-
/// new hashes at heights 101-103.
1402+
/// 101-103 with new ones (same tip height). `recent_history` returns
1403+
/// {94..=103} (last 10 blocks ending at tip) with new hashes at 101-103.
14041404
///
14051405
/// The checkpoint must reflect the reorged chain: new hashes at 101-103,
1406-
/// pre-reorg blocks at ≤100 preserved, new blocks 104-106 present.
1406+
/// pre-reorg blocks at ≤100 preserved.
14071407
#[test]
14081408
fn checkpoint_building_handles_reorg() {
14091409
use bdk_chain::local_chain::LocalChain;
@@ -1430,8 +1430,8 @@ mod tests {
14301430
])
14311431
.unwrap();
14321432

1433-
// recent_history after reorg: 97-106, heights 101-103 have NEW hashes.
1434-
let recent_history: BTreeMap<u32, BlockHash> = (97..=106)
1433+
// recent_history after reorg: 94-103, heights 101-103 have NEW hashes.
1434+
let recent_history: BTreeMap<u32, BlockHash> = (94..=103)
14351435
.map(|h| {
14361436
let seed = if (101..=103).contains(&h) { h + 1000 } else { h };
14371437
(h, hash(seed))
@@ -1441,14 +1441,12 @@ mod tests {
14411441
// Build checkpoint using the same logic as sync_onchain_wallet.
14421442
let mut cp = wallet_cp;
14431443
for (height, block_hash) in &recent_history {
1444-
if *height > cp.height() {
1445-
let block_id = BlockId { height: *height, hash: *block_hash };
1446-
cp = cp.push(block_id).unwrap_or_else(|old| old);
1447-
}
1444+
let block_id = BlockId { height: *height, hash: *block_hash };
1445+
cp = cp.insert(block_id);
14481446
}
14491447

14501448
// Reorged blocks must have the NEW hashes.
1451-
assert_eq!(cp.height(), 106);
1449+
assert_eq!(cp.height(), 103);
14521450
assert_eq!(
14531451
cp.get(101).expect("height 101 must exist").hash(),
14541452
hash(1101),
@@ -1460,11 +1458,6 @@ mod tests {
14601458
// Pre-reorg blocks are preserved.
14611459
assert_eq!(cp.get(100).expect("height 100 must exist").hash(), hash(100));
14621460

1463-
// New blocks above the reorg are present.
1464-
assert!(cp.get(104).is_some());
1465-
assert!(cp.get(105).is_some());
1466-
assert!(cp.get(106).is_some());
1467-
14681461
// The checkpoint must connect cleanly to a LocalChain.
14691462
let (mut chain, _) = LocalChain::from_genesis_hash(genesis.hash);
14701463
chain.apply_update(cp).expect("checkpoint must connect to chain");

tests/integration_tests_rust.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ use common::{
2323
expect_payment_successful_event, expect_splice_pending_event, generate_blocks_and_wait,
2424
generate_listening_addresses, open_channel, open_channel_push_amt, open_channel_with_all,
2525
premine_and_distribute_funds, premine_blocks, prepare_rbf, random_chain_source, random_config,
26-
random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, setup_node,
27-
setup_two_nodes, skip_if_cbf, splice_in_with_all, wait_for_cbf_sync, wait_for_tx,
28-
TestChainSource, TestStoreType, TestSyncStore,
26+
setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, skip_if_cbf,
27+
splice_in_with_all, wait_for_cbf_sync, wait_for_tx, TestChainSource, TestStoreType,
28+
TestSyncStore,
2929
};
3030
use electrsd::corepc_node::Node as BitcoinD;
3131
use electrsd::ElectrsD;

0 commit comments

Comments
 (0)