}
impl LocalChain {
+ pub fn from_blocks<B>(blocks: B) -> Self
+ where
+ B: IntoIterator<Item = BlockId>,
+ {
+ Self {
+ blocks: blocks.into_iter().map(|b| (b.height, b.hash)).collect(),
+ }
+ }
+
pub fn tip(&self) -> Option<BlockId> {
self.blocks
.iter()
}
}
- let mut changeset = BTreeMap::<u32, BlockHash>::new();
- for (height, new_hash) in update {
+ let mut changeset: BTreeMap<u32, Option<BlockHash>> = match invalidate_from_height {
+ Some(first_invalid_height) => {
+ // the first block of height to invalidate should be represented in the update
+ if !update.contains_key(&first_invalid_height) {
+ return Err(UpdateNotConnectedError(first_invalid_height));
+ }
+ self.blocks
+ .range(first_invalid_height..)
+ .map(|(height, _)| (*height, None))
+ .collect()
+ }
+ None => BTreeMap::new(),
+ };
+ for (height, update_hash) in update {
let original_hash = self.blocks.get(height);
- if Some(new_hash) != original_hash {
- changeset.insert(*height, *new_hash);
+ if Some(update_hash) != original_hash {
+ changeset.insert(*height, Some(*update_hash));
}
}
+
Ok(changeset)
}
/// Applies the given `changeset`.
- pub fn apply_changeset(&mut self, mut changeset: ChangeSet) {
- self.blocks.append(&mut changeset)
+ pub fn apply_changeset(&mut self, changeset: ChangeSet) {
+ for (height, blockhash) in changeset {
+ match blockhash {
+ Some(blockhash) => self.blocks.insert(height, blockhash),
+ None => self.blocks.remove(&height),
+ };
+ }
}
/// Updates [`LocalChain`] with an update [`LocalChain`].
}
pub fn initial_changeset(&self) -> ChangeSet {
- self.blocks.clone()
+ self.blocks
+ .iter()
+ .map(|(&height, &hash)| (height, Some(hash)))
+ .collect()
}
pub fn heights(&self) -> BTreeSet<u32> {
/// This is the return value of [`determine_changeset`] and represents changes to [`LocalChain`].
///
/// [`determine_changeset`]: LocalChain::determine_changeset
-type ChangeSet = BTreeMap<u32, BlockHash>;
+pub type ChangeSet = BTreeMap<u32, Option<BlockHash>>;
impl Append for ChangeSet {
fn append(&mut self, mut other: Self) {
/// connect to the existing chain. This error case contains the checkpoint height to include so
/// that the chains can connect.
#[derive(Clone, Debug, PartialEq)]
-pub struct UpdateNotConnectedError(u32);
+pub struct UpdateNotConnectedError(pub u32);
impl core::fmt::Display for UpdateNotConnectedError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
--- /dev/null
+use bdk_chain::local_chain::{LocalChain, UpdateNotConnectedError};
+
+#[macro_use]
+mod common;
+
+#[test]
+fn add_first_tip() {
+ let chain = LocalChain::default();
+ assert_eq!(
+ chain.determine_changeset(&local_chain![(0, h!("A"))]),
+ Ok([(0, Some(h!("A")))].into()),
+ "add first tip"
+ );
+}
+
+#[test]
+fn add_second_tip() {
+ let chain = local_chain![(0, h!("A"))];
+ assert_eq!(
+ chain.determine_changeset(&local_chain![(0, h!("A")), (1, h!("B"))]),
+ Ok([(1, Some(h!("B")))].into())
+ );
+}
+
+#[test]
+fn two_disjoint_chains_cannot_merge() {
+ let chain1 = local_chain![(0, h!("A"))];
+ let chain2 = local_chain![(1, h!("B"))];
+ assert_eq!(
+ chain1.determine_changeset(&chain2),
+ Err(UpdateNotConnectedError(0))
+ );
+}
+
+#[test]
+fn duplicate_chains_should_merge() {
+ let chain1 = local_chain![(0, h!("A"))];
+ let chain2 = local_chain![(0, h!("A"))];
+ assert_eq!(chain1.determine_changeset(&chain2), Ok(Default::default()));
+}
+
+#[test]
+fn can_introduce_older_checkpoints() {
+ let chain1 = local_chain![(2, h!("C")), (3, h!("D"))];
+ let chain2 = local_chain![(1, h!("B")), (2, h!("C"))];
+
+ assert_eq!(
+ chain1.determine_changeset(&chain2),
+ Ok([(1, Some(h!("B")))].into())
+ );
+}
+
+#[test]
+fn fix_blockhash_before_agreement_point() {
+ let chain1 = local_chain![(0, h!("im-wrong")), (1, h!("we-agree"))];
+ let chain2 = local_chain![(0, h!("fix")), (1, h!("we-agree"))];
+
+ assert_eq!(
+ chain1.determine_changeset(&chain2),
+ Ok([(0, Some(h!("fix")))].into())
+ )
+}
+
+/// B and C are in both chain and update
+/// ```
+/// | 0 | 1 | 2 | 3 | 4
+/// chain | B C
+/// update | A B C D
+/// ```
+/// This should succeed with the point of agreement being C and A should be added in addition.
+#[test]
+fn two_points_of_agreement() {
+ let chain1 = local_chain![(1, h!("B")), (2, h!("C"))];
+ let chain2 = local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (3, h!("D"))];
+
+ assert_eq!(
+ chain1.determine_changeset(&chain2),
+ Ok([(0, Some(h!("A"))), (3, Some(h!("D")))].into()),
+ );
+}
+
+/// Update and chain does not connect:
+/// ```
+/// | 0 | 1 | 2 | 3 | 4
+/// chain | B C
+/// update | A B D
+/// ```
+/// This should fail as we cannot figure out whether C & D are on the same chain
+#[test]
+fn update_and_chain_does_not_connect() {
+ let chain1 = local_chain![(1, h!("B")), (2, h!("C"))];
+ let chain2 = local_chain![(0, h!("A")), (1, h!("B")), (3, h!("D"))];
+
+ assert_eq!(
+ chain1.determine_changeset(&chain2),
+ Err(UpdateNotConnectedError(2)),
+ );
+}
+
+/// Transient invalidation:
+/// ```
+/// | 0 | 1 | 2 | 3 | 4 | 5
+/// chain | A B C E
+/// update | A B' C' D
+/// ```
+/// This should succeed and invalidate B,C and E with point of agreement being A.
+#[test]
+fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation() {
+ let chain1 = local_chain![(0, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))];
+ let chain2 = local_chain![(0, h!("A")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))];
+
+ assert_eq!(
+ chain1.determine_changeset(&chain2),
+ Ok([
+ (2, Some(h!("B'"))),
+ (3, Some(h!("C'"))),
+ (4, Some(h!("D"))),
+ (5, None),
+ ]
+ .into())
+ );
+}
+
+/// Transient invalidation:
+/// ```
+/// | 0 | 1 | 2 | 3 | 4
+/// chain | B C E
+/// update | B' C' D
+/// ```
+///
+/// This should succeed and invalidate B, C and E with no point of agreement
+#[test]
+fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation_no_point_of_agreement() {
+ let chain1 = local_chain![(1, h!("B")), (2, h!("C")), (4, h!("E"))];
+ let chain2 = local_chain![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))];
+
+ assert_eq!(
+ chain1.determine_changeset(&chain2),
+ Ok([
+ (1, Some(h!("B'"))),
+ (2, Some(h!("C'"))),
+ (3, Some(h!("D"))),
+ (4, None)
+ ]
+ .into())
+ )
+}
+
+/// Transient invalidation:
+/// ```
+/// | 0 | 1 | 2 | 3 | 4
+/// chain | A B C E
+/// update | B' C' D
+/// ```
+///
+/// This should fail since although it tells us that B and C are invalid it doesn't tell us whether
+/// A was invalid.
+#[test]
+fn invalidation_but_no_connection() {
+ let chain1 = local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (4, h!("E"))];
+ let chain2 = local_chain![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))];
+
+ assert_eq!(
+ chain1.determine_changeset(&chain2),
+ Err(UpdateNotConnectedError(0))
+ )
+}