Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 76 additions & 9 deletions src/chain/cbf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) };
Expand Down Expand Up @@ -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<u32, BlockHash> = (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");
}
}
6 changes: 3 additions & 3 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading