Skip to content

Commit 9a3152e

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 369862f commit 9a3152e

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) };
@@ -1400,11 +1400,11 @@ mod tests {
14001400
/// Test that checkpoint building from `recent_history` handles reorgs.
14011401
///
14021402
/// Scenario: wallet synced to height 103. A 3-block reorg replaces blocks
1403-
/// 101-103 with new ones, and `recent_history` returns {97..=106} with
1404-
/// new hashes at heights 101-103.
1403+
/// 101-103 with new ones (same tip height). `recent_history` returns
1404+
/// {94..=103} (last 10 blocks ending at tip) with new hashes at 101-103.
14051405
///
14061406
/// The checkpoint must reflect the reorged chain: new hashes at 101-103,
1407-
/// pre-reorg blocks at ≤100 preserved, new blocks 104-106 present.
1407+
/// pre-reorg blocks at ≤100 preserved.
14081408
#[test]
14091409
fn checkpoint_building_handles_reorg() {
14101410
use bdk_chain::local_chain::LocalChain;
@@ -1431,8 +1431,8 @@ mod tests {
14311431
])
14321432
.unwrap();
14331433

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

14511449
// Reorged blocks must have the NEW hashes.
1452-
assert_eq!(cp.height(), 106);
1450+
assert_eq!(cp.height(), 103);
14531451
assert_eq!(
14541452
cp.get(101).expect("height 101 must exist").hash(),
14551453
hash(1101),
@@ -1461,11 +1459,6 @@ mod tests {
14611459
// Pre-reorg blocks are preserved.
14621460
assert_eq!(cp.get(100).expect("height 100 must exist").hash(), hash(100));
14631461

1464-
// New blocks above the reorg are present.
1465-
assert!(cp.get(104).is_some());
1466-
assert!(cp.get(105).is_some());
1467-
assert!(cp.get(106).is_some());
1468-
14691462
// The checkpoint must connect cleanly to a LocalChain.
14701463
let (mut chain, _) = LocalChain::from_genesis_hash(genesis.hash);
14711464
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)