Merge bitcoindevkit/bdk#1034: Implement linked-list LocalChain and update chain-src crates/examples
b206a985cffix: Even more refactoring to code and documentation (志宇)bea8e5aff4fix: `TxGraph::missing_blocks` logic (志宇)db15e03bdcfix: improve more docs and more refactoring (志宇)95312d4d05fix: docs and some minor refactoring (志宇)8bf7a997f7Refactor `debug_assertions` checks for `LocalChain` (志宇)315e7e0b4bfix: rm duplicate `bdk_tmp_plan` module (志宇)af705da1a8Add exclusion of example cli `*.db` files in `.gitignore` (志宇)eabeb6ccb1Implement linked-list `LocalChain` and update chain-src crates/examples (志宇) Pull request description: Fixes #997 Replaces #1002 ### Description This PR changes the `LocalChain` implementation to have blocks stored as a linked-list. This allows the data-src thread to hold a shared ref to a single checkpoint and have access to the whole history of checkpoints without cloning or keeping a lock on `LocalChain`. The APIs of `bdk::Wallet`, `esplora` and `electrum` are also updated to reflect these changes. Note that the `esplora` crate is rewritten to anchor txs in the confirmation block (using the esplora API's tx status block_hash). This guarantees 100% consistency between anchor blocks and their transactions (instead of anchoring txs to the latest tip). `ExploraExt` now has separate methods for updating the `TxGraph` and `LocalChain`. A new method `TxGraph::missing_blocks` is introduced for finding "floating anchors" of a `TxGraph` update (given a chain). Additional changes: * `test_local_chain.rs` is refactored to make test cases easier to write. Additional tests are also added. * Examples are updated. * Exclude example-cli `*.db` files in `.gitignore`. * Rm duplicate `bdk_tmp_plan` module. ### Notes to the reviewers This is the smallest possible division of #1002 without resulting in PRs that do not compile. Since we have changed the API of `LocalChain`, we also need to change `esplora`, `electrum` crates and examples alongside `bdk::Wallet`. ### Changelog notice * Implement linked-list `LocalChain`. This allows the data-src thread to hold a shared ref to a single checkpoint and have access to the whole history of checkpoints without cloning or keeping a lock on `LocalChain`. * Rewrote `esplora` chain-src crate to anchor txs to their confirmation blocks (using esplora API's tx-status `block_hash`). ### 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: LLFourn: ACKb206a985cfTree-SHA512: a513eecb4f1aae6a5c06a69854e4492961424312a75a42d74377d363b364e3d52415bc81b4aa3fbc3f369ded19bddd07ab895130ebba288e8a43e9d6186e9fcc
This commit is contained in:
@@ -9,25 +9,20 @@ macro_rules! h {
|
||||
macro_rules! local_chain {
|
||||
[ $(($height:expr, $block_hash:expr)), * ] => {{
|
||||
#[allow(unused_mut)]
|
||||
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*])
|
||||
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*].into_iter().collect())
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! chain {
|
||||
($([$($tt:tt)*]),*) => { chain!( checkpoints: [$([$($tt)*]),*] ) };
|
||||
(checkpoints: $($tail:tt)*) => { chain!( index: TxHeight, checkpoints: $($tail)*) };
|
||||
(index: $ind:ty, checkpoints: [ $([$height:expr, $block_hash:expr]),* ] $(,txids: [$(($txid:expr, $tx_height:expr)),*])?) => {{
|
||||
macro_rules! chain_update {
|
||||
[ $(($height:expr, $hash:expr)), * ] => {{
|
||||
#[allow(unused_mut)]
|
||||
let mut chain = bdk_chain::sparse_chain::SparseChain::<$ind>::from_checkpoints([$(($height, $block_hash).into()),*]);
|
||||
|
||||
$(
|
||||
$(
|
||||
let _ = chain.insert_tx($txid, $tx_height).expect("should succeed");
|
||||
)*
|
||||
)?
|
||||
|
||||
chain
|
||||
bdk_chain::local_chain::Update {
|
||||
tip: bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect())
|
||||
.tip()
|
||||
.expect("must have tip"),
|
||||
introduce_older_blocks: true,
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
@@ -107,11 +107,11 @@ fn insert_relevant_txs() {
|
||||
|
||||
fn test_list_owned_txouts() {
|
||||
// Create Local chains
|
||||
|
||||
let local_chain = (0..150)
|
||||
.map(|i| (i as u32, h!("random")))
|
||||
.collect::<BTreeMap<u32, BlockHash>>();
|
||||
let local_chain = LocalChain::from(local_chain);
|
||||
let local_chain = LocalChain::from(
|
||||
(0..150)
|
||||
.map(|i| (i as u32, h!("random")))
|
||||
.collect::<BTreeMap<u32, BlockHash>>(),
|
||||
);
|
||||
|
||||
// Initiate IndexedTxGraph
|
||||
|
||||
@@ -214,7 +214,8 @@ fn test_list_owned_txouts() {
|
||||
local_chain
|
||||
.blocks()
|
||||
.get(&height)
|
||||
.map(|&hash| BlockId { height, hash })
|
||||
.cloned()
|
||||
.map(|hash| BlockId { height, hash })
|
||||
.map(|anchor_block| ConfirmationHeightAnchor {
|
||||
anchor_block,
|
||||
confirmation_height: anchor_block.height,
|
||||
@@ -234,7 +235,7 @@ fn test_list_owned_txouts() {
|
||||
.blocks()
|
||||
.get(&height)
|
||||
.map(|&hash| BlockId { height, hash })
|
||||
.expect("block must exist");
|
||||
.unwrap_or_else(|| panic!("block must exist at {}", height));
|
||||
let txouts = graph
|
||||
.graph()
|
||||
.filter_chain_txouts(
|
||||
|
||||
@@ -1,180 +1,300 @@
|
||||
use bdk_chain::local_chain::{
|
||||
ChangeSet, InsertBlockNotMatchingError, LocalChain, UpdateNotConnectedError,
|
||||
};
|
||||
use bdk_chain::local_chain::{CannotConnectError, ChangeSet, InsertBlockError, LocalChain, Update};
|
||||
use bitcoin::BlockHash;
|
||||
|
||||
#[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"
|
||||
);
|
||||
#[derive(Debug)]
|
||||
struct TestLocalChain<'a> {
|
||||
name: &'static str,
|
||||
chain: LocalChain,
|
||||
update: Update,
|
||||
exp: ExpectedResult<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum ExpectedResult<'a> {
|
||||
Ok {
|
||||
changeset: &'a [(u32, Option<BlockHash>)],
|
||||
init_changeset: &'a [(u32, Option<BlockHash>)],
|
||||
},
|
||||
Err(CannotConnectError),
|
||||
}
|
||||
|
||||
impl<'a> TestLocalChain<'a> {
|
||||
fn run(mut self) {
|
||||
println!("[TestLocalChain] test: {}", self.name);
|
||||
let got_changeset = match self.chain.apply_update(self.update) {
|
||||
Ok(changeset) => changeset,
|
||||
Err(got_err) => {
|
||||
assert_eq!(
|
||||
ExpectedResult::Err(got_err),
|
||||
self.exp,
|
||||
"{}: unexpected error",
|
||||
self.name
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match self.exp {
|
||||
ExpectedResult::Ok {
|
||||
changeset,
|
||||
init_changeset,
|
||||
} => {
|
||||
assert_eq!(
|
||||
got_changeset,
|
||||
changeset.iter().cloned().collect(),
|
||||
"{}: unexpected changeset",
|
||||
self.name
|
||||
);
|
||||
assert_eq!(
|
||||
self.chain.initial_changeset(),
|
||||
init_changeset.iter().cloned().collect(),
|
||||
"{}: unexpected initial changeset",
|
||||
self.name
|
||||
);
|
||||
}
|
||||
ExpectedResult::Err(err) => panic!(
|
||||
"{}: expected error ({}), got non-error result: {:?}",
|
||||
self.name, err, got_changeset
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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())
|
||||
);
|
||||
fn update_local_chain() {
|
||||
[
|
||||
TestLocalChain {
|
||||
name: "add first tip",
|
||||
chain: local_chain![],
|
||||
update: chain_update![(0, h!("A"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(0, Some(h!("A")))],
|
||||
init_changeset: &[(0, Some(h!("A")))],
|
||||
},
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "add second tip",
|
||||
chain: local_chain![(0, h!("A"))],
|
||||
update: chain_update![(0, h!("A")), (1, h!("B"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(1, Some(h!("B")))],
|
||||
init_changeset: &[(0, Some(h!("A"))), (1, Some(h!("B")))],
|
||||
},
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "two disjoint chains cannot merge",
|
||||
chain: local_chain![(0, h!("A"))],
|
||||
update: chain_update![(1, h!("B"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError {
|
||||
try_include_height: 0,
|
||||
}),
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "two disjoint chains cannot merge (existing chain longer)",
|
||||
chain: local_chain![(1, h!("A"))],
|
||||
update: chain_update![(0, h!("B"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError {
|
||||
try_include_height: 1,
|
||||
}),
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "duplicate chains should merge",
|
||||
chain: local_chain![(0, h!("A"))],
|
||||
update: chain_update![(0, h!("A"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[],
|
||||
init_changeset: &[(0, Some(h!("A")))],
|
||||
},
|
||||
},
|
||||
// Introduce an older checkpoint (B)
|
||||
// | 0 | 1 | 2 | 3
|
||||
// chain | C D
|
||||
// update | B C
|
||||
TestLocalChain {
|
||||
name: "can introduce older checkpoint",
|
||||
chain: local_chain![(2, h!("C")), (3, h!("D"))],
|
||||
update: chain_update![(1, h!("B")), (2, h!("C"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(1, Some(h!("B")))],
|
||||
init_changeset: &[(1, Some(h!("B"))), (2, Some(h!("C"))), (3, Some(h!("D")))],
|
||||
},
|
||||
},
|
||||
// Introduce an older checkpoint (A) that is not directly behind PoA
|
||||
// | 2 | 3 | 4
|
||||
// chain | B C
|
||||
// update | A C
|
||||
TestLocalChain {
|
||||
name: "can introduce older checkpoint 2",
|
||||
chain: local_chain![(3, h!("B")), (4, h!("C"))],
|
||||
update: chain_update![(2, h!("A")), (4, h!("C"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(2, Some(h!("A")))],
|
||||
init_changeset: &[(2, Some(h!("A"))), (3, Some(h!("B"))), (4, Some(h!("C")))],
|
||||
}
|
||||
},
|
||||
// Introduce an older checkpoint (B) that is not the oldest checkpoint
|
||||
// | 1 | 2 | 3
|
||||
// chain | A C
|
||||
// update | B C
|
||||
TestLocalChain {
|
||||
name: "can introduce older checkpoint 3",
|
||||
chain: local_chain![(1, h!("A")), (3, h!("C"))],
|
||||
update: chain_update![(2, h!("B")), (3, h!("C"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(2, Some(h!("B")))],
|
||||
init_changeset: &[(1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
|
||||
}
|
||||
},
|
||||
// Introduce two older checkpoints below the PoA
|
||||
// | 1 | 2 | 3
|
||||
// chain | C
|
||||
// update | A B C
|
||||
TestLocalChain {
|
||||
name: "introduce two older checkpoints below PoA",
|
||||
chain: local_chain![(3, h!("C"))],
|
||||
update: chain_update![(1, h!("A")), (2, h!("B")), (3, h!("C"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(1, Some(h!("A"))), (2, Some(h!("B")))],
|
||||
init_changeset: &[(1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
|
||||
},
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "fix blockhash before agreement point",
|
||||
chain: local_chain![(0, h!("im-wrong")), (1, h!("we-agree"))],
|
||||
update: chain_update![(0, h!("fix")), (1, h!("we-agree"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(0, Some(h!("fix")))],
|
||||
init_changeset: &[(0, Some(h!("fix"))), (1, Some(h!("we-agree")))],
|
||||
},
|
||||
},
|
||||
// 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.
|
||||
TestLocalChain {
|
||||
name: "two points of agreement",
|
||||
chain: local_chain![(1, h!("B")), (2, h!("C"))],
|
||||
update: chain_update![(0, h!("A")), (1, h!("B")), (2, h!("C")), (3, h!("D"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(0, Some(h!("A"))), (3, Some(h!("D")))],
|
||||
init_changeset: &[
|
||||
(0, Some(h!("A"))),
|
||||
(1, Some(h!("B"))),
|
||||
(2, Some(h!("C"))),
|
||||
(3, Some(h!("D"))),
|
||||
],
|
||||
},
|
||||
},
|
||||
// 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
|
||||
TestLocalChain {
|
||||
name: "update and chain does not connect",
|
||||
chain: local_chain![(1, h!("B")), (2, h!("C"))],
|
||||
update: chain_update![(0, h!("A")), (1, h!("B")), (3, h!("D"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError {
|
||||
try_include_height: 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.
|
||||
TestLocalChain {
|
||||
name: "transitive invalidation applies to checkpoints higher than invalidation",
|
||||
chain: local_chain![(0, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
|
||||
update: chain_update![(0, h!("A")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[
|
||||
(2, Some(h!("B'"))),
|
||||
(3, Some(h!("C'"))),
|
||||
(4, Some(h!("D"))),
|
||||
(5, None),
|
||||
],
|
||||
init_changeset: &[
|
||||
(0, Some(h!("A"))),
|
||||
(2, Some(h!("B'"))),
|
||||
(3, Some(h!("C'"))),
|
||||
(4, Some(h!("D"))),
|
||||
],
|
||||
},
|
||||
},
|
||||
// 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
|
||||
TestLocalChain {
|
||||
name: "transitive invalidation applies to checkpoints higher than invalidation no point of agreement",
|
||||
chain: local_chain![(1, h!("B")), (2, h!("C")), (4, h!("E"))],
|
||||
update: chain_update![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[
|
||||
(1, Some(h!("B'"))),
|
||||
(2, Some(h!("C'"))),
|
||||
(3, Some(h!("D"))),
|
||||
(4, None)
|
||||
],
|
||||
init_changeset: &[
|
||||
(1, Some(h!("B'"))),
|
||||
(2, Some(h!("C'"))),
|
||||
(3, Some(h!("D"))),
|
||||
],
|
||||
},
|
||||
},
|
||||
// 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.
|
||||
TestLocalChain {
|
||||
name: "invalidation but no connection",
|
||||
chain: local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (4, h!("E"))],
|
||||
update: chain_update![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError { try_include_height: 0 }),
|
||||
},
|
||||
// Introduce blocks between two points of agreement
|
||||
// | 0 | 1 | 2 | 3 | 4 | 5
|
||||
// chain | A B D E
|
||||
// update | A C E F
|
||||
TestLocalChain {
|
||||
name: "introduce blocks between two points of agreement",
|
||||
chain: local_chain![(0, h!("A")), (1, h!("B")), (3, h!("D")), (4, h!("E"))],
|
||||
update: chain_update![(0, h!("A")), (2, h!("C")), (4, h!("E")), (5, h!("F"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[
|
||||
(2, Some(h!("C"))),
|
||||
(5, Some(h!("F"))),
|
||||
],
|
||||
init_changeset: &[
|
||||
(0, Some(h!("A"))),
|
||||
(1, Some(h!("B"))),
|
||||
(2, Some(h!("C"))),
|
||||
(3, Some(h!("D"))),
|
||||
(4, Some(h!("E"))),
|
||||
(5, Some(h!("F"))),
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(TestLocalChain::run);
|
||||
}
|
||||
|
||||
#[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))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_block() {
|
||||
fn local_chain_insert_block() {
|
||||
struct TestCase {
|
||||
original: LocalChain,
|
||||
insert: (u32, BlockHash),
|
||||
expected_result: Result<ChangeSet, InsertBlockNotMatchingError>,
|
||||
expected_result: Result<ChangeSet, InsertBlockError>,
|
||||
expected_final: LocalChain,
|
||||
}
|
||||
|
||||
@@ -206,7 +326,7 @@ fn insert_block() {
|
||||
TestCase {
|
||||
original: local_chain![(2, h!("K"))],
|
||||
insert: (2, h!("J")),
|
||||
expected_result: Err(InsertBlockNotMatchingError {
|
||||
expected_result: Err(InsertBlockError {
|
||||
height: 2,
|
||||
original_hash: h!("K"),
|
||||
update_hash: h!("J"),
|
||||
|
||||
@@ -4,7 +4,7 @@ use bdk_chain::{
|
||||
collections::*,
|
||||
local_chain::LocalChain,
|
||||
tx_graph::{Additions, TxGraph},
|
||||
Append, BlockId, ChainPosition, ConfirmationHeightAnchor,
|
||||
Anchor, Append, BlockId, ChainPosition, ConfirmationHeightAnchor,
|
||||
};
|
||||
use bitcoin::{
|
||||
hashes::Hash, BlockHash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid,
|
||||
@@ -697,7 +697,7 @@ fn test_chain_spends() {
|
||||
let _ = graph.insert_anchor(
|
||||
tx.txid(),
|
||||
ConfirmationHeightAnchor {
|
||||
anchor_block: tip,
|
||||
anchor_block: tip.block_id(),
|
||||
confirmation_height: *ht,
|
||||
},
|
||||
);
|
||||
@@ -705,10 +705,10 @@ fn test_chain_spends() {
|
||||
|
||||
// Assert that confirmed spends are returned correctly.
|
||||
assert_eq!(
|
||||
graph.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 0)),
|
||||
graph.get_chain_spend(&local_chain, tip.block_id(), OutPoint::new(tx_0.txid(), 0)),
|
||||
Some((
|
||||
ChainPosition::Confirmed(&ConfirmationHeightAnchor {
|
||||
anchor_block: tip,
|
||||
anchor_block: tip.block_id(),
|
||||
confirmation_height: 98
|
||||
}),
|
||||
tx_1.txid(),
|
||||
@@ -717,17 +717,17 @@ fn test_chain_spends() {
|
||||
|
||||
// Check if chain position is returned correctly.
|
||||
assert_eq!(
|
||||
graph.get_chain_position(&local_chain, tip, tx_0.txid()),
|
||||
graph.get_chain_position(&local_chain, tip.block_id(), tx_0.txid()),
|
||||
// Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))),
|
||||
Some(ChainPosition::Confirmed(&ConfirmationHeightAnchor {
|
||||
anchor_block: tip,
|
||||
anchor_block: tip.block_id(),
|
||||
confirmation_height: 95
|
||||
}))
|
||||
);
|
||||
|
||||
// Even if unconfirmed tx has a last_seen of 0, it can still be part of a chain spend.
|
||||
assert_eq!(
|
||||
graph.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1)),
|
||||
graph.get_chain_spend(&local_chain, tip.block_id(), OutPoint::new(tx_0.txid(), 1)),
|
||||
Some((ChainPosition::Unconfirmed(0), tx_2.txid())),
|
||||
);
|
||||
|
||||
@@ -737,7 +737,7 @@ fn test_chain_spends() {
|
||||
// Check chain spend returned correctly.
|
||||
assert_eq!(
|
||||
graph
|
||||
.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1))
|
||||
.get_chain_spend(&local_chain, tip.block_id(), OutPoint::new(tx_0.txid(), 1))
|
||||
.unwrap(),
|
||||
(ChainPosition::Unconfirmed(1234567), tx_2.txid())
|
||||
);
|
||||
@@ -754,7 +754,7 @@ fn test_chain_spends() {
|
||||
|
||||
// Because this tx conflicts with an already confirmed transaction, chain position should return none.
|
||||
assert!(graph
|
||||
.get_chain_position(&local_chain, tip, tx_1_conflict.txid())
|
||||
.get_chain_position(&local_chain, tip.block_id(), tx_1_conflict.txid())
|
||||
.is_none());
|
||||
|
||||
// Another conflicting tx that conflicts with tx_2.
|
||||
@@ -773,7 +773,7 @@ fn test_chain_spends() {
|
||||
// This should return a valid observation with correct last seen.
|
||||
assert_eq!(
|
||||
graph
|
||||
.get_chain_position(&local_chain, tip, tx_2_conflict.txid())
|
||||
.get_chain_position(&local_chain, tip.block_id(), tx_2_conflict.txid())
|
||||
.expect("position expected"),
|
||||
ChainPosition::Unconfirmed(1234568)
|
||||
);
|
||||
@@ -781,14 +781,14 @@ fn test_chain_spends() {
|
||||
// Chain_spend now catches the new transaction as the spend.
|
||||
assert_eq!(
|
||||
graph
|
||||
.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1))
|
||||
.get_chain_spend(&local_chain, tip.block_id(), OutPoint::new(tx_0.txid(), 1))
|
||||
.expect("expect observation"),
|
||||
(ChainPosition::Unconfirmed(1234568), tx_2_conflict.txid())
|
||||
);
|
||||
|
||||
// Chain position of the `tx_2` is now none, as it is older than `tx_2_conflict`
|
||||
assert!(graph
|
||||
.get_chain_position(&local_chain, tip, tx_2.txid())
|
||||
.get_chain_position(&local_chain, tip.block_id(), tx_2.txid())
|
||||
.is_none());
|
||||
}
|
||||
|
||||
@@ -822,3 +822,136 @@ fn test_additions_last_seen_append() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_blocks() {
|
||||
/// An anchor implementation for testing, made up of `(the_anchor_block, random_data)`.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, core::hash::Hash)]
|
||||
struct TestAnchor(BlockId);
|
||||
|
||||
impl Anchor for TestAnchor {
|
||||
fn anchor_block(&self) -> BlockId {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
struct Scenario<'a> {
|
||||
name: &'a str,
|
||||
graph: TxGraph<TestAnchor>,
|
||||
chain: LocalChain,
|
||||
exp_heights: &'a [u32],
|
||||
}
|
||||
|
||||
const fn new_anchor(height: u32, hash: BlockHash) -> TestAnchor {
|
||||
TestAnchor(BlockId { height, hash })
|
||||
}
|
||||
|
||||
fn new_scenario<'a>(
|
||||
name: &'a str,
|
||||
graph_anchors: &'a [(Txid, TestAnchor)],
|
||||
chain: &'a [(u32, BlockHash)],
|
||||
exp_heights: &'a [u32],
|
||||
) -> Scenario<'a> {
|
||||
Scenario {
|
||||
name,
|
||||
graph: {
|
||||
let mut g = TxGraph::default();
|
||||
for (txid, anchor) in graph_anchors {
|
||||
let _ = g.insert_anchor(*txid, anchor.clone());
|
||||
}
|
||||
g
|
||||
},
|
||||
chain: {
|
||||
let mut c = LocalChain::default();
|
||||
for (height, hash) in chain {
|
||||
let _ = c.insert_block(BlockId {
|
||||
height: *height,
|
||||
hash: *hash,
|
||||
});
|
||||
}
|
||||
c
|
||||
},
|
||||
exp_heights,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(scenarios: &[Scenario]) {
|
||||
for scenario in scenarios {
|
||||
let Scenario {
|
||||
name,
|
||||
graph,
|
||||
chain,
|
||||
exp_heights,
|
||||
} = scenario;
|
||||
|
||||
let heights = graph.missing_heights(chain).collect::<Vec<_>>();
|
||||
assert_eq!(&heights, exp_heights, "scenario: {}", name);
|
||||
}
|
||||
}
|
||||
|
||||
run(&[
|
||||
new_scenario(
|
||||
"2 txs with the same anchor (2:B) which is missing from chain",
|
||||
&[
|
||||
(h!("tx_1"), new_anchor(2, h!("B"))),
|
||||
(h!("tx_2"), new_anchor(2, h!("B"))),
|
||||
],
|
||||
&[(1, h!("A")), (3, h!("C"))],
|
||||
&[2],
|
||||
),
|
||||
new_scenario(
|
||||
"2 txs with different anchors at the same height, one of the anchors is missing",
|
||||
&[
|
||||
(h!("tx_1"), new_anchor(2, h!("B1"))),
|
||||
(h!("tx_2"), new_anchor(2, h!("B2"))),
|
||||
],
|
||||
&[(1, h!("A")), (2, h!("B1"))],
|
||||
&[],
|
||||
),
|
||||
new_scenario(
|
||||
"tx with 2 anchors of same height which are missing from the chain",
|
||||
&[
|
||||
(h!("tx"), new_anchor(3, h!("C1"))),
|
||||
(h!("tx"), new_anchor(3, h!("C2"))),
|
||||
],
|
||||
&[(1, h!("A")), (4, h!("D"))],
|
||||
&[3],
|
||||
),
|
||||
new_scenario(
|
||||
"tx with 2 anchors at the same height, chain has this height but does not match either anchor",
|
||||
&[
|
||||
(h!("tx"), new_anchor(4, h!("D1"))),
|
||||
(h!("tx"), new_anchor(4, h!("D2"))),
|
||||
],
|
||||
&[(4, h!("D3")), (5, h!("E"))],
|
||||
&[],
|
||||
),
|
||||
new_scenario(
|
||||
"tx with 2 anchors at different heights, one anchor exists in chain, should return nothing",
|
||||
&[
|
||||
(h!("tx"), new_anchor(3, h!("C"))),
|
||||
(h!("tx"), new_anchor(4, h!("D"))),
|
||||
],
|
||||
&[(4, h!("D")), (5, h!("E"))],
|
||||
&[],
|
||||
),
|
||||
new_scenario(
|
||||
"tx with 2 anchors at different heights, first height is already in chain with different hash, iterator should only return 2nd height",
|
||||
&[
|
||||
(h!("tx"), new_anchor(5, h!("E1"))),
|
||||
(h!("tx"), new_anchor(6, h!("F1"))),
|
||||
],
|
||||
&[(4, h!("D")), (5, h!("E")), (7, h!("G"))],
|
||||
&[6],
|
||||
),
|
||||
new_scenario(
|
||||
"tx with 2 anchors at different heights, neither height is in chain, both heights should be returned",
|
||||
&[
|
||||
(h!("tx"), new_anchor(3, h!("C"))),
|
||||
(h!("tx"), new_anchor(4, h!("D"))),
|
||||
],
|
||||
&[(1, h!("A")), (2, h!("B"))],
|
||||
&[3, 4],
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user