Skip to content

Commit 96e7c45

Browse files
committed
feat(chain): add CheckPoint::from_block_ids convenience method
1 parent f9dad51 commit 96e7c45

File tree

2 files changed

+106
-2
lines changed

2 files changed

+106
-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: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
use bdk_chain::local_chain::{
2-
AlterCheckPointError, CannotConnectError, ChangeSet, LocalChain, Update,
1+
use bdk_chain::{
2+
local_chain::{
3+
AlterCheckPointError, CannotConnectError, ChangeSet, CheckPoint, LocalChain, Update,
4+
},
5+
BlockId,
36
};
47
use bitcoin::BlockHash;
58

@@ -350,3 +353,82 @@ fn local_chain_insert_block() {
350353
assert_eq!(chain, t.expected_final, "[{}] unexpected final chain", i,);
351354
}
352355
}
356+
357+
#[test]
358+
fn checkpoint_from_block_ids() {
359+
struct TestCase<'a> {
360+
name: &'a str,
361+
blocks: &'a [(u32, BlockHash)],
362+
exp_result: Result<(), Option<(u32, BlockHash)>>,
363+
}
364+
365+
let test_cases = [
366+
TestCase {
367+
name: "in_order",
368+
blocks: &[(0, h!("A")), (1, h!("B")), (3, h!("D"))],
369+
exp_result: Ok(()),
370+
},
371+
TestCase {
372+
name: "with_duplicates",
373+
blocks: &[(1, h!("B")), (2, h!("C")), (2, h!("C'"))],
374+
exp_result: Err(Some((2, h!("C")))),
375+
},
376+
TestCase {
377+
name: "not_in_order",
378+
blocks: &[(1, h!("B")), (3, h!("D")), (2, h!("C"))],
379+
exp_result: Err(Some((3, h!("D")))),
380+
},
381+
TestCase {
382+
name: "empty",
383+
blocks: &[],
384+
exp_result: Err(None),
385+
},
386+
TestCase {
387+
name: "single",
388+
blocks: &[(21, h!("million"))],
389+
exp_result: Ok(()),
390+
},
391+
];
392+
393+
for (i, t) in test_cases.into_iter().enumerate() {
394+
println!("running test case {}: '{}'", i, t.name);
395+
let result = CheckPoint::from_block_ids(
396+
t.blocks
397+
.iter()
398+
.map(|&(height, hash)| BlockId { height, hash }),
399+
);
400+
match t.exp_result {
401+
Ok(_) => {
402+
assert!(result.is_ok(), "[{}:{}] should be Ok", i, t.name);
403+
let result_vec = {
404+
let mut v = result
405+
.unwrap()
406+
.into_iter()
407+
.map(|cp| (cp.height(), cp.hash()))
408+
.collect::<Vec<_>>();
409+
v.reverse();
410+
v
411+
};
412+
assert_eq!(
413+
&result_vec, t.blocks,
414+
"[{}:{}] not equal to original block ids",
415+
i, t.name
416+
);
417+
}
418+
Err(exp_last) => {
419+
assert!(result.is_err(), "[{}:{}] should be Err", i, t.name);
420+
let err = result.unwrap_err();
421+
assert_eq!(
422+
err.as_ref()
423+
.map(|last_cp| (last_cp.height(), last_cp.hash())),
424+
exp_last,
425+
"[{}:{}] error's last cp height should be {:?}, got {:?}",
426+
i,
427+
t.name,
428+
exp_last,
429+
err
430+
);
431+
}
432+
}
433+
}
434+
}

0 commit comments

Comments
 (0)