Skip to content

Commit e805152

Browse files
committed
test(chain): add test for merge_chains
1 parent 833dcb6 commit e805152

File tree

1 file changed

+146
-1
lines changed

1 file changed

+146
-1
lines changed

crates/chain/tests/test_local_chain.rs

Lines changed: 146 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,151 @@ fn local_chain_insert_header() {
474474
}
475475
}
476476

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

0 commit comments

Comments
 (0)