Skip to content

Commit 40f0765

Browse files
Merge #1276: Add LocalChain::disconnect_from method
bf67519 feat(chain): add `LocalChain::disconnect_from` method (志宇) Pull request description: Closes #1271 ### Description Add a method for disconnecting a chain of blocks starting from the given `BlockId`. ### Notes to the reviewers I want to have this for utreexo/utreexod#110 ### Changelog notice Added * `LocalChain::disconnect_from` method to evict a chain of blocks starting from a given `BlockId`. ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### New Features: * [x] I've added tests for the new feature * [x] I've added docs for the new feature ACKs for top commit: danielabrozzoni: ACK bf67519 Tree-SHA512: e6bd213b49b553355370994567722ad2c3460d11fcd81adc65a85e5d03822d3c38e4a4d7f967044991cf0187845467b67d035bf8904871f9fcc4ea61be761ef7
2 parents b6422f7 + bf67519 commit 40f0765

File tree

2 files changed

+96
-1
lines changed

2 files changed

+96
-1
lines changed

crates/chain/src/local_chain.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,28 @@ impl LocalChain {
420420
Ok(changeset)
421421
}
422422

423+
/// Removes blocks from (and inclusive of) the given `block_id`.
424+
///
425+
/// This will remove blocks with a height equal or greater than `block_id`, but only if
426+
/// `block_id` exists in the chain.
427+
///
428+
/// # Errors
429+
///
430+
/// This will fail with [`MissingGenesisError`] if the caller attempts to disconnect from the
431+
/// genesis block.
432+
pub fn disconnect_from(&mut self, block_id: BlockId) -> Result<ChangeSet, MissingGenesisError> {
433+
if self.index.get(&block_id.height) != Some(&block_id.hash) {
434+
return Ok(ChangeSet::default());
435+
}
436+
437+
let changeset = self
438+
.index
439+
.range(block_id.height..)
440+
.map(|(&height, _)| (height, None))
441+
.collect::<ChangeSet>();
442+
self.apply_changeset(&changeset).map(|_| changeset)
443+
}
444+
423445
/// Reindex the heights in the chain from (and including) `from` height
424446
fn reindex(&mut self, from: u32) {
425447
let _ = self.index.split_off(&from);

crates/chain/tests/test_local_chain.rs

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use bdk_chain::local_chain::{
2-
AlterCheckPointError, CannotConnectError, ChangeSet, LocalChain, Update,
2+
AlterCheckPointError, CannotConnectError, ChangeSet, LocalChain, MissingGenesisError, Update,
33
};
44
use bitcoin::BlockHash;
55

@@ -350,3 +350,76 @@ fn local_chain_insert_block() {
350350
assert_eq!(chain, t.expected_final, "[{}] unexpected final chain", i,);
351351
}
352352
}
353+
354+
#[test]
355+
fn local_chain_disconnect_from() {
356+
struct TestCase {
357+
name: &'static str,
358+
original: LocalChain,
359+
disconnect_from: (u32, BlockHash),
360+
exp_result: Result<ChangeSet, MissingGenesisError>,
361+
exp_final: LocalChain,
362+
}
363+
364+
let test_cases = [
365+
TestCase {
366+
name: "try_replace_genesis_should_fail",
367+
original: local_chain![(0, h!("_"))],
368+
disconnect_from: (0, h!("_")),
369+
exp_result: Err(MissingGenesisError),
370+
exp_final: local_chain![(0, h!("_"))],
371+
},
372+
TestCase {
373+
name: "try_replace_genesis_should_fail_2",
374+
original: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
375+
disconnect_from: (0, h!("_")),
376+
exp_result: Err(MissingGenesisError),
377+
exp_final: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
378+
},
379+
TestCase {
380+
name: "from_does_not_exist",
381+
original: local_chain![(0, h!("_")), (3, h!("C"))],
382+
disconnect_from: (2, h!("B")),
383+
exp_result: Ok(ChangeSet::default()),
384+
exp_final: local_chain![(0, h!("_")), (3, h!("C"))],
385+
},
386+
TestCase {
387+
name: "from_has_different_blockhash",
388+
original: local_chain![(0, h!("_")), (2, h!("B"))],
389+
disconnect_from: (2, h!("not_B")),
390+
exp_result: Ok(ChangeSet::default()),
391+
exp_final: local_chain![(0, h!("_")), (2, h!("B"))],
392+
},
393+
TestCase {
394+
name: "disconnect_one",
395+
original: local_chain![(0, h!("_")), (2, h!("B"))],
396+
disconnect_from: (2, h!("B")),
397+
exp_result: Ok(ChangeSet::from_iter([(2, None)])),
398+
exp_final: local_chain![(0, h!("_"))],
399+
},
400+
TestCase {
401+
name: "disconnect_three",
402+
original: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C")), (4, h!("D"))],
403+
disconnect_from: (2, h!("B")),
404+
exp_result: Ok(ChangeSet::from_iter([(2, None), (3, None), (4, None)])),
405+
exp_final: local_chain![(0, h!("_"))],
406+
},
407+
];
408+
409+
for (i, t) in test_cases.into_iter().enumerate() {
410+
println!("Case {}: {}", i, t.name);
411+
412+
let mut chain = t.original;
413+
let result = chain.disconnect_from(t.disconnect_from.into());
414+
assert_eq!(
415+
result, t.exp_result,
416+
"[{}:{}] unexpected changeset result",
417+
i, t.name
418+
);
419+
assert_eq!(
420+
chain, t.exp_final,
421+
"[{}:{}] unexpected final chain",
422+
i, t.name
423+
);
424+
}
425+
}

0 commit comments

Comments
 (0)