Skip to content

Commit 46bd424

Browse files
committed
test(chain): add test for merge_chains
1 parent 06b6d7e commit 46bd424

File tree

1 file changed

+155
-1
lines changed

1 file changed

+155
-1
lines changed

crates/chain/tests/test_local_chain.rs

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use bdk_chain::{
1111
BlockId,
1212
};
1313
use bdk_testenv::{chain_update, hash, local_chain};
14-
use bitcoin::{block::Header, hashes::Hash, BlockHash};
14+
use bitcoin::{block::Header, hashes::Hash, BlockHash, CompactTarget, TxMerkleNode};
1515
use proptest::prelude::*;
1616

1717
#[derive(Debug)]
@@ -474,6 +474,160 @@ fn local_chain_insert_header() {
474474
}
475475
}
476476

477+
/// Validates `merge_chains` behavior on chains that contain placeholder checkpoints (`data: None`).
478+
///
479+
/// Placeholders are created when a `CheckPoint`’s `prev_blockhash` references a block at a height
480+
/// with no stored checkpoint. This test ensures `merge_chains` handles them correctly and that the
481+
/// resulting chain never exposes a placeholder checkpoint.
482+
#[test]
483+
fn merge_chains_handles_placeholders() {
484+
fn header(prev_blockhash: bitcoin::BlockHash, nonce: u32) -> Header {
485+
Header {
486+
version: bitcoin::block::Version::default(),
487+
prev_blockhash,
488+
merkle_root: TxMerkleNode::all_zeros(),
489+
time: 0,
490+
bits: CompactTarget::default(),
491+
nonce,
492+
}
493+
}
494+
495+
fn local_chain(blocks: Vec<(u32, Header)>) -> LocalChain<Header> {
496+
LocalChain::from_blocks(blocks.into_iter().collect::<BTreeMap<_, _>>())
497+
.expect("chain must have genesis block")
498+
}
499+
500+
fn update_chain(blocks: &[(u32, Header)]) -> CheckPoint<Header> {
501+
CheckPoint::from_blocks(blocks.iter().copied()).expect("checkpoint must be valid")
502+
}
503+
504+
let a = header(hash!("genesis"), 0);
505+
let b = header(a.block_hash(), 0);
506+
let c = header(b.block_hash(), 0);
507+
let d = header(c.block_hash(), 0);
508+
let e = header(d.block_hash(), 0);
509+
510+
// Set a different `nonce` for conflicting `Header`s to ensure different `BlockHash`.
511+
let c_conflict = header(b.block_hash(), 1);
512+
let d_conflict = header(c_conflict.block_hash(), 1);
513+
514+
struct TestCase {
515+
name: &'static str,
516+
updates: Vec<CheckPoint<Header>>,
517+
invalidate_heights: Vec<u32>,
518+
expected_placeholder_heights: Vec<u32>,
519+
expected_chain: LocalChain<Header>,
520+
}
521+
522+
let test_cases = [
523+
// Test case 1: Create a placeholder for B via C and a placeholder for D via E.
524+
TestCase {
525+
name: "insert_placeholder",
526+
updates: vec![update_chain(&[(0, a), (2, c), (4, e)])],
527+
invalidate_heights: vec![],
528+
expected_placeholder_heights: vec![1, 3],
529+
expected_chain: local_chain(vec![(0, a), (2, c), (4, e)]),
530+
},
531+
// Test cast 2: Create a placeholder for B via C, then update provides conflicting C'.
532+
TestCase {
533+
name: "conflict_at_tip_keeps_placeholder",
534+
updates: vec![
535+
update_chain(&[(0, a), (2, c)]),
536+
update_chain(&[(2, c_conflict)]),
537+
],
538+
invalidate_heights: vec![],
539+
expected_placeholder_heights: vec![1],
540+
expected_chain: local_chain(vec![(0, a), (1, b), (2, c_conflict)]),
541+
},
542+
// Test case 3: Create placeholder for C via D.
543+
TestCase {
544+
name: "conflict_at_filled_height",
545+
updates: vec![update_chain(&[(0, a), (3, d)])],
546+
invalidate_heights: vec![],
547+
expected_placeholder_heights: vec![2],
548+
expected_chain: local_chain(vec![(0, a), (3, d)]),
549+
},
550+
// Test case 4: Create placeholder for C via D, then insert conflicting C' which should
551+
// drop D and replace C.
552+
TestCase {
553+
name: "conflict_at_filled_height",
554+
updates: vec![
555+
update_chain(&[(0, a), (3, d)]),
556+
update_chain(&[(0, a), (2, c_conflict)]),
557+
],
558+
invalidate_heights: vec![],
559+
expected_placeholder_heights: vec![1],
560+
expected_chain: local_chain(vec![(0, a), (2, c_conflict)]),
561+
},
562+
// Test case 5: Create placeholder for B via C, then invalidate C.
563+
TestCase {
564+
name: "invalidate_tip_falls_back",
565+
updates: vec![update_chain(&[(0, a), (2, c)])],
566+
invalidate_heights: vec![2],
567+
expected_placeholder_heights: vec![],
568+
expected_chain: local_chain(vec![(0, a)]),
569+
},
570+
// Test case 6: Create placeholder for C via D, then insert D' which has `prev_blockhash`
571+
// that does not point to C. TODO: Handle error?
572+
TestCase {
573+
name: "expected_error",
574+
updates: vec![
575+
update_chain(&[(0, a), (3, d)]),
576+
update_chain(&[(3, d_conflict)]),
577+
],
578+
invalidate_heights: vec![],
579+
expected_placeholder_heights: vec![2],
580+
expected_chain: local_chain(vec![(0, a), (3, d)]),
581+
},
582+
];
583+
584+
for (i, t) in test_cases.into_iter().enumerate() {
585+
let mut chain = local_chain(vec![(0, a)]);
586+
for upd in t.updates {
587+
// If `apply_update` errors, it is because the new chain cannot be merged. So it should
588+
// follow that this validates behavior if the final `expected_chain` state is correct.
589+
if chain.apply_update(upd).is_ok() {
590+
if !t.invalidate_heights.is_empty() {
591+
let cs: ChangeSet<Header> = t
592+
.invalidate_heights
593+
.iter()
594+
.copied()
595+
.map(|h| (h, None))
596+
.collect();
597+
chain.apply_changeset(&cs).expect("changeset should apply");
598+
}
599+
600+
// Ensure we never end up with a placeholder tip.
601+
assert!(
602+
chain.tip().data_ref().is_some(),
603+
"[{}] {}: tip must always be materialized",
604+
i,
605+
t.name
606+
);
607+
}
608+
}
609+
610+
let mut placeholder_heights = chain
611+
.tip()
612+
.iter()
613+
.filter(|cp| cp.data_ref().is_none())
614+
.map(|cp| cp.height())
615+
.collect::<Vec<_>>();
616+
placeholder_heights.sort();
617+
assert_eq!(
618+
placeholder_heights, t.expected_placeholder_heights,
619+
"[{}] {}: placeholder height mismatch",
620+
i, t.name
621+
);
622+
623+
assert_eq!(
624+
chain, t.expected_chain,
625+
"[{}] {}: unexpected final chain",
626+
i, t.name
627+
);
628+
}
629+
}
630+
477631
#[test]
478632
fn local_chain_disconnect_from() {
479633
struct TestCase {

0 commit comments

Comments
 (0)