Skip to content

Commit 2b61a12

Browse files
committed
feat(chain): add CheckPoint::from_block_ids convenience method
1 parent 40f0765 commit 2b61a12

File tree

2 files changed

+107
-2
lines changed

2 files changed

+107
-2
lines changed

crates/chain/src/local_chain.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,28 @@ impl CheckPoint {
3939
Self(Arc::new(CPInner { block, prev: None }))
4040
}
4141

42+
/// Construct a checkpoint from a list of [`BlockId`]s in ascending height order.
43+
///
44+
/// # Errors
45+
///
46+
/// This method will error if any of the follow occurs:
47+
///
48+
/// - The `blocks` iterator is empty, in which case, the error will be `None`.
49+
/// - The `blocks` iterator is not in ascending height order.
50+
/// - The `blocks` iterator contains multiple [`BlockId`]s of the same height.
51+
///
52+
/// The error type is the last successful checkpoint constructed (if any).
53+
pub fn from_block_ids(
54+
block_ids: impl IntoIterator<Item = BlockId>,
55+
) -> Result<Self, Option<Self>> {
56+
let mut blocks = block_ids.into_iter();
57+
let mut acc = CheckPoint::new(blocks.next().ok_or(None)?);
58+
for id in blocks {
59+
acc = acc.push(id).map_err(Some)?;
60+
}
61+
Ok(acc)
62+
}
63+
4264
/// Construct a checkpoint from the given `header` and block `height`.
4365
///
4466
/// If `header` is of the genesis block, the checkpoint won't have a [`prev`] node. Otherwise,

crates/chain/tests/test_local_chain.rs

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
use bdk_chain::local_chain::{
2-
AlterCheckPointError, CannotConnectError, ChangeSet, LocalChain, MissingGenesisError, Update,
1+
use bdk_chain::{
2+
local_chain::{
3+
AlterCheckPointError, CannotConnectError, ChangeSet, CheckPoint, LocalChain,
4+
MissingGenesisError, Update,
5+
},
6+
BlockId,
37
};
48
use bitcoin::BlockHash;
59

@@ -423,3 +427,82 @@ fn local_chain_disconnect_from() {
423427
);
424428
}
425429
}
430+
431+
#[test]
432+
fn checkpoint_from_block_ids() {
433+
struct TestCase<'a> {
434+
name: &'a str,
435+
blocks: &'a [(u32, BlockHash)],
436+
exp_result: Result<(), Option<(u32, BlockHash)>>,
437+
}
438+
439+
let test_cases = [
440+
TestCase {
441+
name: "in_order",
442+
blocks: &[(0, h!("A")), (1, h!("B")), (3, h!("D"))],
443+
exp_result: Ok(()),
444+
},
445+
TestCase {
446+
name: "with_duplicates",
447+
blocks: &[(1, h!("B")), (2, h!("C")), (2, h!("C'"))],
448+
exp_result: Err(Some((2, h!("C")))),
449+
},
450+
TestCase {
451+
name: "not_in_order",
452+
blocks: &[(1, h!("B")), (3, h!("D")), (2, h!("C"))],
453+
exp_result: Err(Some((3, h!("D")))),
454+
},
455+
TestCase {
456+
name: "empty",
457+
blocks: &[],
458+
exp_result: Err(None),
459+
},
460+
TestCase {
461+
name: "single",
462+
blocks: &[(21, h!("million"))],
463+
exp_result: Ok(()),
464+
},
465+
];
466+
467+
for (i, t) in test_cases.into_iter().enumerate() {
468+
println!("running test case {}: '{}'", i, t.name);
469+
let result = CheckPoint::from_block_ids(
470+
t.blocks
471+
.iter()
472+
.map(|&(height, hash)| BlockId { height, hash }),
473+
);
474+
match t.exp_result {
475+
Ok(_) => {
476+
assert!(result.is_ok(), "[{}:{}] should be Ok", i, t.name);
477+
let result_vec = {
478+
let mut v = result
479+
.unwrap()
480+
.into_iter()
481+
.map(|cp| (cp.height(), cp.hash()))
482+
.collect::<Vec<_>>();
483+
v.reverse();
484+
v
485+
};
486+
assert_eq!(
487+
&result_vec, t.blocks,
488+
"[{}:{}] not equal to original block ids",
489+
i, t.name
490+
);
491+
}
492+
Err(exp_last) => {
493+
assert!(result.is_err(), "[{}:{}] should be Err", i, t.name);
494+
let err = result.unwrap_err();
495+
assert_eq!(
496+
err.as_ref()
497+
.map(|last_cp| (last_cp.height(), last_cp.hash())),
498+
exp_last,
499+
"[{}:{}] error's last cp height should be {:?}, got {:?}",
500+
i,
501+
t.name,
502+
exp_last,
503+
err
504+
);
505+
}
506+
}
507+
}
508+
}

0 commit comments

Comments
 (0)