test(esplora): move esplora tests into src files
Since we want to keep these methods private.
This commit is contained in:
parent
a6e613e6b9
commit
519cd75d23
@ -25,6 +25,7 @@ miniscript = { version = "11.0.0", optional = true, default-features = false }
|
|||||||
bdk_testenv = { path = "../testenv", default_features = false }
|
bdk_testenv = { path = "../testenv", default_features = false }
|
||||||
electrsd = { version= "0.27.1", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
electrsd = { version= "0.27.1", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
||||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
|
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
|
||||||
|
anyhow = "1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["std", "async-https", "blocking"]
|
default = ["std", "async-https", "blocking"]
|
||||||
|
@ -139,8 +139,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
|||||||
/// block-based chain-sources). Therefore it's better to be conservative when setting the tip (use
|
/// block-based chain-sources). Therefore it's better to be conservative when setting the tip (use
|
||||||
/// an earlier tip rather than a later tip) otherwise the caller may accidentally skip blocks when
|
/// an earlier tip rather than a later tip) otherwise the caller may accidentally skip blocks when
|
||||||
/// alternating between chain-sources.
|
/// alternating between chain-sources.
|
||||||
#[doc(hidden)]
|
async fn init_chain_update(
|
||||||
pub async fn init_chain_update(
|
|
||||||
client: &esplora_client::AsyncClient,
|
client: &esplora_client::AsyncClient,
|
||||||
local_tip: &CheckPoint,
|
local_tip: &CheckPoint,
|
||||||
) -> Result<BTreeMap<u32, BlockHash>, Error> {
|
) -> Result<BTreeMap<u32, BlockHash>, Error> {
|
||||||
@ -183,8 +182,7 @@ pub async fn init_chain_update(
|
|||||||
///
|
///
|
||||||
/// A checkpoint is considered "missing" if an anchor (of `anchors`) points to a height without an
|
/// A checkpoint is considered "missing" if an anchor (of `anchors`) points to a height without an
|
||||||
/// existing checkpoint/block under `local_tip` or `update_blocks`.
|
/// existing checkpoint/block under `local_tip` or `update_blocks`.
|
||||||
#[doc(hidden)]
|
async fn finalize_chain_update<A: Anchor>(
|
||||||
pub async fn finalize_chain_update<A: Anchor>(
|
|
||||||
client: &esplora_client::AsyncClient,
|
client: &esplora_client::AsyncClient,
|
||||||
local_tip: &CheckPoint,
|
local_tip: &CheckPoint,
|
||||||
anchors: &BTreeSet<(A, Txid)>,
|
anchors: &BTreeSet<(A, Txid)>,
|
||||||
@ -243,8 +241,7 @@ pub async fn finalize_chain_update<A: Anchor>(
|
|||||||
|
|
||||||
/// This performs a full scan to get an update for the [`TxGraph`] and
|
/// This performs a full scan to get an update for the [`TxGraph`] and
|
||||||
/// [`KeychainTxOutIndex`](bdk_chain::keychain::KeychainTxOutIndex).
|
/// [`KeychainTxOutIndex`](bdk_chain::keychain::KeychainTxOutIndex).
|
||||||
#[doc(hidden)]
|
async fn full_scan_for_index_and_graph<K: Ord + Clone + Send>(
|
||||||
pub async fn full_scan_for_index_and_graph<K: Ord + Clone + Send>(
|
|
||||||
client: &esplora_client::AsyncClient,
|
client: &esplora_client::AsyncClient,
|
||||||
keychain_spks: BTreeMap<
|
keychain_spks: BTreeMap<
|
||||||
K,
|
K,
|
||||||
@ -339,8 +336,7 @@ pub async fn full_scan_for_index_and_graph<K: Ord + Clone + Send>(
|
|||||||
Ok((graph, last_active_indexes))
|
Ok((graph, last_active_indexes))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
async fn sync_for_index_and_graph(
|
||||||
pub async fn sync_for_index_and_graph(
|
|
||||||
client: &esplora_client::AsyncClient,
|
client: &esplora_client::AsyncClient,
|
||||||
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
|
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
|
||||||
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
|
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
|
||||||
@ -414,3 +410,184 @@ pub async fn sync_for_index_and_graph(
|
|||||||
|
|
||||||
Ok(graph)
|
Ok(graph)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::{collections::BTreeSet, time::Duration};
|
||||||
|
|
||||||
|
use bdk_chain::{
|
||||||
|
bitcoin::{hashes::Hash, Txid},
|
||||||
|
local_chain::LocalChain,
|
||||||
|
BlockId,
|
||||||
|
};
|
||||||
|
use bdk_testenv::TestEnv;
|
||||||
|
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||||
|
use esplora_client::Builder;
|
||||||
|
|
||||||
|
use crate::async_ext::{finalize_chain_update, init_chain_update};
|
||||||
|
|
||||||
|
macro_rules! h {
|
||||||
|
($index:literal) => {{
|
||||||
|
bdk_chain::bitcoin::hashes::Hash::hash($index.as_bytes())
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that update does not remove heights (from original), and all anchor heights are included.
|
||||||
|
#[tokio::test]
|
||||||
|
pub async fn test_finalize_chain_update() -> anyhow::Result<()> {
|
||||||
|
struct TestCase<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
/// Initial blockchain height to start the env with.
|
||||||
|
initial_env_height: u32,
|
||||||
|
/// Initial checkpoint heights to start with.
|
||||||
|
initial_cps: &'a [u32],
|
||||||
|
/// The final blockchain height of the env.
|
||||||
|
final_env_height: u32,
|
||||||
|
/// The anchors to test with: `(height, txid)`. Only the height is provided as we can fetch
|
||||||
|
/// the blockhash from the env.
|
||||||
|
anchors: &'a [(u32, Txid)],
|
||||||
|
}
|
||||||
|
|
||||||
|
let test_cases = [
|
||||||
|
TestCase {
|
||||||
|
name: "chain_extends",
|
||||||
|
initial_env_height: 60,
|
||||||
|
initial_cps: &[59, 60],
|
||||||
|
final_env_height: 90,
|
||||||
|
anchors: &[],
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "introduce_older_heights",
|
||||||
|
initial_env_height: 50,
|
||||||
|
initial_cps: &[10, 15],
|
||||||
|
final_env_height: 50,
|
||||||
|
anchors: &[(11, h!("A")), (14, h!("B"))],
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "introduce_older_heights_after_chain_extends",
|
||||||
|
initial_env_height: 50,
|
||||||
|
initial_cps: &[10, 15],
|
||||||
|
final_env_height: 100,
|
||||||
|
anchors: &[(11, h!("A")), (14, h!("B"))],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (i, t) in test_cases.into_iter().enumerate() {
|
||||||
|
println!("[{}] running test case: {}", i, t.name);
|
||||||
|
|
||||||
|
let env = TestEnv::new()?;
|
||||||
|
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||||
|
let client = Builder::new(base_url.as_str()).build_async()?;
|
||||||
|
|
||||||
|
// set env to `initial_env_height`
|
||||||
|
if let Some(to_mine) = t
|
||||||
|
.initial_env_height
|
||||||
|
.checked_sub(env.make_checkpoint_tip().height())
|
||||||
|
{
|
||||||
|
env.mine_blocks(to_mine as _, None)?;
|
||||||
|
}
|
||||||
|
while client.get_height().await? < t.initial_env_height {
|
||||||
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
// craft initial `local_chain`
|
||||||
|
let local_chain = {
|
||||||
|
let (mut chain, _) = LocalChain::from_genesis_hash(env.genesis_hash()?);
|
||||||
|
let chain_tip = chain.tip();
|
||||||
|
let update_blocks = init_chain_update(&client, &chain_tip).await?;
|
||||||
|
let update_anchors = t
|
||||||
|
.initial_cps
|
||||||
|
.iter()
|
||||||
|
.map(|&height| -> anyhow::Result<_> {
|
||||||
|
Ok((
|
||||||
|
BlockId {
|
||||||
|
height,
|
||||||
|
hash: env.bitcoind.client.get_block_hash(height as _)?,
|
||||||
|
},
|
||||||
|
Txid::all_zeros(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<anyhow::Result<BTreeSet<_>>>()?;
|
||||||
|
let chain_update =
|
||||||
|
finalize_chain_update(&client, &chain_tip, &update_anchors, update_blocks)
|
||||||
|
.await?;
|
||||||
|
chain.apply_update(chain_update)?;
|
||||||
|
chain
|
||||||
|
};
|
||||||
|
println!("local chain height: {}", local_chain.tip().height());
|
||||||
|
|
||||||
|
// extend env chain
|
||||||
|
if let Some(to_mine) = t
|
||||||
|
.final_env_height
|
||||||
|
.checked_sub(env.make_checkpoint_tip().height())
|
||||||
|
{
|
||||||
|
env.mine_blocks(to_mine as _, None)?;
|
||||||
|
}
|
||||||
|
while client.get_height().await? < t.final_env_height {
|
||||||
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
// craft update
|
||||||
|
let update = {
|
||||||
|
let local_tip = local_chain.tip();
|
||||||
|
let update_blocks = init_chain_update(&client, &local_tip).await?;
|
||||||
|
let update_anchors = t
|
||||||
|
.anchors
|
||||||
|
.iter()
|
||||||
|
.map(|&(height, txid)| -> anyhow::Result<_> {
|
||||||
|
Ok((
|
||||||
|
BlockId {
|
||||||
|
height,
|
||||||
|
hash: env.bitcoind.client.get_block_hash(height as _)?,
|
||||||
|
},
|
||||||
|
txid,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<anyhow::Result<_>>()?;
|
||||||
|
finalize_chain_update(&client, &local_tip, &update_anchors, update_blocks).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
// apply update
|
||||||
|
let mut updated_local_chain = local_chain.clone();
|
||||||
|
updated_local_chain.apply_update(update)?;
|
||||||
|
println!(
|
||||||
|
"updated local chain height: {}",
|
||||||
|
updated_local_chain.tip().height()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
{
|
||||||
|
let initial_heights = local_chain
|
||||||
|
.iter_checkpoints()
|
||||||
|
.map(|cp| cp.height())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
let updated_heights = updated_local_chain
|
||||||
|
.iter_checkpoints()
|
||||||
|
.map(|cp| cp.height())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
updated_heights.is_superset(&initial_heights)
|
||||||
|
},
|
||||||
|
"heights from the initial chain must all be in the updated chain",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
{
|
||||||
|
let exp_anchor_heights = t
|
||||||
|
.anchors
|
||||||
|
.iter()
|
||||||
|
.map(|(h, _)| *h)
|
||||||
|
.chain(t.initial_cps.iter().copied())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
let anchor_heights = updated_local_chain
|
||||||
|
.iter_checkpoints()
|
||||||
|
.map(|cp| cp.height())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
anchor_heights.is_superset(&exp_anchor_heights)
|
||||||
|
},
|
||||||
|
"anchor heights must all be in updated chain",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -148,8 +148,7 @@ impl EsploraExt for esplora_client::BlockingClient {
|
|||||||
/// block-based chain-sources). Therefore it's better to be conservative when setting the tip (use
|
/// block-based chain-sources). Therefore it's better to be conservative when setting the tip (use
|
||||||
/// an earlier tip rather than a later tip) otherwise the caller may accidentally skip blocks when
|
/// an earlier tip rather than a later tip) otherwise the caller may accidentally skip blocks when
|
||||||
/// alternating between chain-sources.
|
/// alternating between chain-sources.
|
||||||
#[doc(hidden)]
|
fn init_chain_update_blocking(
|
||||||
pub fn init_chain_update_blocking(
|
|
||||||
client: &esplora_client::BlockingClient,
|
client: &esplora_client::BlockingClient,
|
||||||
local_tip: &CheckPoint,
|
local_tip: &CheckPoint,
|
||||||
) -> Result<BTreeMap<u32, BlockHash>, Error> {
|
) -> Result<BTreeMap<u32, BlockHash>, Error> {
|
||||||
@ -191,8 +190,7 @@ pub fn init_chain_update_blocking(
|
|||||||
///
|
///
|
||||||
/// A checkpoint is considered "missing" if an anchor (of `anchors`) points to a height without an
|
/// A checkpoint is considered "missing" if an anchor (of `anchors`) points to a height without an
|
||||||
/// existing checkpoint/block under `local_tip` or `update_blocks`.
|
/// existing checkpoint/block under `local_tip` or `update_blocks`.
|
||||||
#[doc(hidden)]
|
fn finalize_chain_update_blocking<A: Anchor>(
|
||||||
pub fn finalize_chain_update_blocking<A: Anchor>(
|
|
||||||
client: &esplora_client::BlockingClient,
|
client: &esplora_client::BlockingClient,
|
||||||
local_tip: &CheckPoint,
|
local_tip: &CheckPoint,
|
||||||
anchors: &BTreeSet<(A, Txid)>,
|
anchors: &BTreeSet<(A, Txid)>,
|
||||||
@ -251,8 +249,7 @@ pub fn finalize_chain_update_blocking<A: Anchor>(
|
|||||||
|
|
||||||
/// This performs a full scan to get an update for the [`TxGraph`] and
|
/// This performs a full scan to get an update for the [`TxGraph`] and
|
||||||
/// [`KeychainTxOutIndex`](bdk_chain::keychain::KeychainTxOutIndex).
|
/// [`KeychainTxOutIndex`](bdk_chain::keychain::KeychainTxOutIndex).
|
||||||
#[doc(hidden)]
|
fn full_scan_for_index_and_graph_blocking<K: Ord + Clone>(
|
||||||
pub fn full_scan_for_index_and_graph_blocking<K: Ord + Clone>(
|
|
||||||
client: &esplora_client::BlockingClient,
|
client: &esplora_client::BlockingClient,
|
||||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||||
stop_gap: usize,
|
stop_gap: usize,
|
||||||
@ -347,8 +344,7 @@ pub fn full_scan_for_index_and_graph_blocking<K: Ord + Clone>(
|
|||||||
Ok((tx_graph, last_active_indices))
|
Ok((tx_graph, last_active_indices))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
fn sync_for_index_and_graph_blocking(
|
||||||
pub fn sync_for_index_and_graph_blocking(
|
|
||||||
client: &esplora_client::BlockingClient,
|
client: &esplora_client::BlockingClient,
|
||||||
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
||||||
txids: impl IntoIterator<Item = Txid>,
|
txids: impl IntoIterator<Item = Txid>,
|
||||||
@ -431,3 +427,391 @@ pub fn sync_for_index_and_graph_blocking(
|
|||||||
|
|
||||||
Ok(tx_graph)
|
Ok(tx_graph)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::blocking_ext::{finalize_chain_update_blocking, init_chain_update_blocking};
|
||||||
|
use bdk_chain::bitcoin::hashes::Hash;
|
||||||
|
use bdk_chain::bitcoin::Txid;
|
||||||
|
use bdk_chain::local_chain::LocalChain;
|
||||||
|
use bdk_chain::BlockId;
|
||||||
|
use bdk_testenv::TestEnv;
|
||||||
|
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||||
|
use esplora_client::{BlockHash, Builder};
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
macro_rules! h {
|
||||||
|
($index:literal) => {{
|
||||||
|
bdk_chain::bitcoin::hashes::Hash::hash($index.as_bytes())
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! local_chain {
|
||||||
|
[ $(($height:expr, $block_hash:expr)), * ] => {{
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*].into_iter().collect())
|
||||||
|
.expect("chain must have genesis block")
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that update does not remove heights (from original), and all anchor heights are included.
|
||||||
|
#[test]
|
||||||
|
pub fn test_finalize_chain_update() -> anyhow::Result<()> {
|
||||||
|
struct TestCase<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
/// Initial blockchain height to start the env with.
|
||||||
|
initial_env_height: u32,
|
||||||
|
/// Initial checkpoint heights to start with.
|
||||||
|
initial_cps: &'a [u32],
|
||||||
|
/// The final blockchain height of the env.
|
||||||
|
final_env_height: u32,
|
||||||
|
/// The anchors to test with: `(height, txid)`. Only the height is provided as we can fetch
|
||||||
|
/// the blockhash from the env.
|
||||||
|
anchors: &'a [(u32, Txid)],
|
||||||
|
}
|
||||||
|
|
||||||
|
let test_cases = [
|
||||||
|
TestCase {
|
||||||
|
name: "chain_extends",
|
||||||
|
initial_env_height: 60,
|
||||||
|
initial_cps: &[59, 60],
|
||||||
|
final_env_height: 90,
|
||||||
|
anchors: &[],
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "introduce_older_heights",
|
||||||
|
initial_env_height: 50,
|
||||||
|
initial_cps: &[10, 15],
|
||||||
|
final_env_height: 50,
|
||||||
|
anchors: &[(11, h!("A")), (14, h!("B"))],
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "introduce_older_heights_after_chain_extends",
|
||||||
|
initial_env_height: 50,
|
||||||
|
initial_cps: &[10, 15],
|
||||||
|
final_env_height: 100,
|
||||||
|
anchors: &[(11, h!("A")), (14, h!("B"))],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (i, t) in test_cases.into_iter().enumerate() {
|
||||||
|
println!("[{}] running test case: {}", i, t.name);
|
||||||
|
|
||||||
|
let env = TestEnv::new()?;
|
||||||
|
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||||
|
let client = Builder::new(base_url.as_str()).build_blocking();
|
||||||
|
|
||||||
|
// set env to `initial_env_height`
|
||||||
|
if let Some(to_mine) = t
|
||||||
|
.initial_env_height
|
||||||
|
.checked_sub(env.make_checkpoint_tip().height())
|
||||||
|
{
|
||||||
|
env.mine_blocks(to_mine as _, None)?;
|
||||||
|
}
|
||||||
|
while client.get_height()? < t.initial_env_height {
|
||||||
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
// craft initial `local_chain`
|
||||||
|
let local_chain = {
|
||||||
|
let (mut chain, _) = LocalChain::from_genesis_hash(env.genesis_hash()?);
|
||||||
|
let chain_tip = chain.tip();
|
||||||
|
let update_blocks = init_chain_update_blocking(&client, &chain_tip)?;
|
||||||
|
let update_anchors = t
|
||||||
|
.initial_cps
|
||||||
|
.iter()
|
||||||
|
.map(|&height| -> anyhow::Result<_> {
|
||||||
|
Ok((
|
||||||
|
BlockId {
|
||||||
|
height,
|
||||||
|
hash: env.bitcoind.client.get_block_hash(height as _)?,
|
||||||
|
},
|
||||||
|
Txid::all_zeros(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<anyhow::Result<BTreeSet<_>>>()?;
|
||||||
|
let chain_update = finalize_chain_update_blocking(
|
||||||
|
&client,
|
||||||
|
&chain_tip,
|
||||||
|
&update_anchors,
|
||||||
|
update_blocks,
|
||||||
|
)?;
|
||||||
|
chain.apply_update(chain_update)?;
|
||||||
|
chain
|
||||||
|
};
|
||||||
|
println!("local chain height: {}", local_chain.tip().height());
|
||||||
|
|
||||||
|
// extend env chain
|
||||||
|
if let Some(to_mine) = t
|
||||||
|
.final_env_height
|
||||||
|
.checked_sub(env.make_checkpoint_tip().height())
|
||||||
|
{
|
||||||
|
env.mine_blocks(to_mine as _, None)?;
|
||||||
|
}
|
||||||
|
while client.get_height()? < t.final_env_height {
|
||||||
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
// craft update
|
||||||
|
let update = {
|
||||||
|
let local_tip = local_chain.tip();
|
||||||
|
let update_blocks = init_chain_update_blocking(&client, &local_tip)?;
|
||||||
|
let update_anchors = t
|
||||||
|
.anchors
|
||||||
|
.iter()
|
||||||
|
.map(|&(height, txid)| -> anyhow::Result<_> {
|
||||||
|
Ok((
|
||||||
|
BlockId {
|
||||||
|
height,
|
||||||
|
hash: env.bitcoind.client.get_block_hash(height as _)?,
|
||||||
|
},
|
||||||
|
txid,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<anyhow::Result<_>>()?;
|
||||||
|
finalize_chain_update_blocking(&client, &local_tip, &update_anchors, update_blocks)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// apply update
|
||||||
|
let mut updated_local_chain = local_chain.clone();
|
||||||
|
updated_local_chain.apply_update(update)?;
|
||||||
|
println!(
|
||||||
|
"updated local chain height: {}",
|
||||||
|
updated_local_chain.tip().height()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
{
|
||||||
|
let initial_heights = local_chain
|
||||||
|
.iter_checkpoints()
|
||||||
|
.map(|cp| cp.height())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
let updated_heights = updated_local_chain
|
||||||
|
.iter_checkpoints()
|
||||||
|
.map(|cp| cp.height())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
updated_heights.is_superset(&initial_heights)
|
||||||
|
},
|
||||||
|
"heights from the initial chain must all be in the updated chain",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
{
|
||||||
|
let exp_anchor_heights = t
|
||||||
|
.anchors
|
||||||
|
.iter()
|
||||||
|
.map(|(h, _)| *h)
|
||||||
|
.chain(t.initial_cps.iter().copied())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
let anchor_heights = updated_local_chain
|
||||||
|
.iter_checkpoints()
|
||||||
|
.map(|cp| cp.height())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
anchor_heights.is_superset(&exp_anchor_heights)
|
||||||
|
},
|
||||||
|
"anchor heights must all be in updated chain",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_local_chain() -> anyhow::Result<()> {
|
||||||
|
const TIP_HEIGHT: u32 = 50;
|
||||||
|
|
||||||
|
let env = TestEnv::new()?;
|
||||||
|
let blocks = {
|
||||||
|
let bitcoind_client = &env.bitcoind.client;
|
||||||
|
assert_eq!(bitcoind_client.get_block_count()?, 1);
|
||||||
|
[
|
||||||
|
(0, bitcoind_client.get_block_hash(0)?),
|
||||||
|
(1, bitcoind_client.get_block_hash(1)?),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.chain((2..).zip(env.mine_blocks((TIP_HEIGHT - 1) as usize, None)?))
|
||||||
|
.collect::<BTreeMap<_, _>>()
|
||||||
|
};
|
||||||
|
// so new blocks can be seen by Electrs
|
||||||
|
let env = env.reset_electrsd()?;
|
||||||
|
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||||
|
let client = Builder::new(base_url.as_str()).build_blocking();
|
||||||
|
|
||||||
|
struct TestCase {
|
||||||
|
name: &'static str,
|
||||||
|
chain: LocalChain,
|
||||||
|
request_heights: &'static [u32],
|
||||||
|
exp_update_heights: &'static [u32],
|
||||||
|
}
|
||||||
|
|
||||||
|
let test_cases = [
|
||||||
|
TestCase {
|
||||||
|
name: "request_later_blocks",
|
||||||
|
chain: local_chain![(0, blocks[&0]), (21, blocks[&21])],
|
||||||
|
request_heights: &[22, 25, 28],
|
||||||
|
exp_update_heights: &[21, 22, 25, 28],
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "request_prev_blocks",
|
||||||
|
chain: local_chain![(0, blocks[&0]), (1, blocks[&1]), (5, blocks[&5])],
|
||||||
|
request_heights: &[4],
|
||||||
|
exp_update_heights: &[4, 5],
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "request_prev_blocks_2",
|
||||||
|
chain: local_chain![(0, blocks[&0]), (1, blocks[&1]), (10, blocks[&10])],
|
||||||
|
request_heights: &[4, 6],
|
||||||
|
exp_update_heights: &[4, 6, 10],
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "request_later_and_prev_blocks",
|
||||||
|
chain: local_chain![(0, blocks[&0]), (7, blocks[&7]), (11, blocks[&11])],
|
||||||
|
request_heights: &[8, 9, 15],
|
||||||
|
exp_update_heights: &[8, 9, 11, 15],
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "request_tip_only",
|
||||||
|
chain: local_chain![(0, blocks[&0]), (5, blocks[&5]), (49, blocks[&49])],
|
||||||
|
request_heights: &[TIP_HEIGHT],
|
||||||
|
exp_update_heights: &[49],
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "request_nothing",
|
||||||
|
chain: local_chain![(0, blocks[&0]), (13, blocks[&13]), (23, blocks[&23])],
|
||||||
|
request_heights: &[],
|
||||||
|
exp_update_heights: &[23],
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "request_nothing_during_reorg",
|
||||||
|
chain: local_chain![(0, blocks[&0]), (13, blocks[&13]), (23, h!("23"))],
|
||||||
|
request_heights: &[],
|
||||||
|
exp_update_heights: &[13, 23],
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "request_nothing_during_reorg_2",
|
||||||
|
chain: local_chain![
|
||||||
|
(0, blocks[&0]),
|
||||||
|
(21, blocks[&21]),
|
||||||
|
(22, h!("22")),
|
||||||
|
(23, h!("23"))
|
||||||
|
],
|
||||||
|
request_heights: &[],
|
||||||
|
exp_update_heights: &[21, 22, 23],
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "request_prev_blocks_during_reorg",
|
||||||
|
chain: local_chain![
|
||||||
|
(0, blocks[&0]),
|
||||||
|
(21, blocks[&21]),
|
||||||
|
(22, h!("22")),
|
||||||
|
(23, h!("23"))
|
||||||
|
],
|
||||||
|
request_heights: &[17, 20],
|
||||||
|
exp_update_heights: &[17, 20, 21, 22, 23],
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "request_later_blocks_during_reorg",
|
||||||
|
chain: local_chain![
|
||||||
|
(0, blocks[&0]),
|
||||||
|
(9, blocks[&9]),
|
||||||
|
(22, h!("22")),
|
||||||
|
(23, h!("23"))
|
||||||
|
],
|
||||||
|
request_heights: &[25, 27],
|
||||||
|
exp_update_heights: &[9, 22, 23, 25, 27],
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "request_later_blocks_during_reorg_2",
|
||||||
|
chain: local_chain![(0, blocks[&0]), (9, h!("9"))],
|
||||||
|
request_heights: &[10],
|
||||||
|
exp_update_heights: &[0, 9, 10],
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "request_later_and_prev_blocks_during_reorg",
|
||||||
|
chain: local_chain![(0, blocks[&0]), (1, blocks[&1]), (9, h!("9"))],
|
||||||
|
request_heights: &[8, 11],
|
||||||
|
exp_update_heights: &[1, 8, 9, 11],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (i, t) in test_cases.into_iter().enumerate() {
|
||||||
|
println!("Case {}: {}", i, t.name);
|
||||||
|
let mut chain = t.chain;
|
||||||
|
let cp_tip = chain.tip();
|
||||||
|
|
||||||
|
let new_blocks = init_chain_update_blocking(&client, &cp_tip).map_err(|err| {
|
||||||
|
anyhow::format_err!("[{}:{}] `init_chain_update` failed: {}", i, t.name, err)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mock_anchors = t
|
||||||
|
.request_heights
|
||||||
|
.iter()
|
||||||
|
.map(|&h| {
|
||||||
|
let anchor_blockhash: BlockHash = bdk_chain::bitcoin::hashes::Hash::hash(
|
||||||
|
&format!("hash_at_height_{}", h).into_bytes(),
|
||||||
|
);
|
||||||
|
let txid: Txid = bdk_chain::bitcoin::hashes::Hash::hash(
|
||||||
|
&format!("txid_at_height_{}", h).into_bytes(),
|
||||||
|
);
|
||||||
|
let anchor = BlockId {
|
||||||
|
height: h,
|
||||||
|
hash: anchor_blockhash,
|
||||||
|
};
|
||||||
|
(anchor, txid)
|
||||||
|
})
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
|
||||||
|
let chain_update =
|
||||||
|
finalize_chain_update_blocking(&client, &cp_tip, &mock_anchors, new_blocks)?;
|
||||||
|
let update_blocks = chain_update
|
||||||
|
.tip
|
||||||
|
.iter()
|
||||||
|
.map(|cp| cp.block_id())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
|
||||||
|
let exp_update_blocks = t
|
||||||
|
.exp_update_heights
|
||||||
|
.iter()
|
||||||
|
.map(|&height| {
|
||||||
|
let hash = blocks[&height];
|
||||||
|
BlockId { height, hash }
|
||||||
|
})
|
||||||
|
.chain(
|
||||||
|
// Electrs Esplora `get_block` call fetches 10 blocks which is included in the
|
||||||
|
// update
|
||||||
|
blocks
|
||||||
|
.range(TIP_HEIGHT - 9..)
|
||||||
|
.map(|(&height, &hash)| BlockId { height, hash }),
|
||||||
|
)
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
update_blocks.is_superset(&exp_update_blocks),
|
||||||
|
"[{}:{}] unexpected update",
|
||||||
|
i,
|
||||||
|
t.name
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = chain
|
||||||
|
.apply_update(chain_update)
|
||||||
|
.unwrap_or_else(|err| panic!("[{}:{}] update failed to apply: {}", i, t.name, err));
|
||||||
|
|
||||||
|
// all requested heights must exist in the final chain
|
||||||
|
for height in t.request_heights {
|
||||||
|
let exp_blockhash = blocks.get(height).expect("block must exist in bitcoind");
|
||||||
|
assert_eq!(
|
||||||
|
chain.get(*height).map(|cp| cp.hash()),
|
||||||
|
Some(*exp_blockhash),
|
||||||
|
"[{}:{}] block {}:{} must exist in final chain",
|
||||||
|
i,
|
||||||
|
t.name,
|
||||||
|
height,
|
||||||
|
exp_blockhash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
use bdk_chain::bitcoin::hashes::Hash;
|
|
||||||
use bdk_chain::local_chain::LocalChain;
|
|
||||||
use bdk_chain::BlockId;
|
|
||||||
use bdk_esplora::EsploraAsyncExt;
|
use bdk_esplora::EsploraAsyncExt;
|
||||||
use electrsd::bitcoind::anyhow;
|
use electrsd::bitcoind::anyhow;
|
||||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||||
@ -13,175 +10,6 @@ use std::time::Duration;
|
|||||||
use bdk_chain::bitcoin::{Address, Amount, Txid};
|
use bdk_chain::bitcoin::{Address, Amount, Txid};
|
||||||
use bdk_testenv::TestEnv;
|
use bdk_testenv::TestEnv;
|
||||||
|
|
||||||
macro_rules! h {
|
|
||||||
($index:literal) => {{
|
|
||||||
bdk_chain::bitcoin::hashes::Hash::hash($index.as_bytes())
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensure that update does not remove heights (from original), and all anchor heights are included.
|
|
||||||
#[tokio::test]
|
|
||||||
pub async fn test_finalize_chain_update() -> anyhow::Result<()> {
|
|
||||||
struct TestCase<'a> {
|
|
||||||
name: &'a str,
|
|
||||||
/// Initial blockchain height to start the env with.
|
|
||||||
initial_env_height: u32,
|
|
||||||
/// Initial checkpoint heights to start with.
|
|
||||||
initial_cps: &'a [u32],
|
|
||||||
/// The final blockchain height of the env.
|
|
||||||
final_env_height: u32,
|
|
||||||
/// The anchors to test with: `(height, txid)`. Only the height is provided as we can fetch
|
|
||||||
/// the blockhash from the env.
|
|
||||||
anchors: &'a [(u32, Txid)],
|
|
||||||
}
|
|
||||||
|
|
||||||
let test_cases = [
|
|
||||||
TestCase {
|
|
||||||
name: "chain_extends",
|
|
||||||
initial_env_height: 60,
|
|
||||||
initial_cps: &[59, 60],
|
|
||||||
final_env_height: 90,
|
|
||||||
anchors: &[],
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
name: "introduce_older_heights",
|
|
||||||
initial_env_height: 50,
|
|
||||||
initial_cps: &[10, 15],
|
|
||||||
final_env_height: 50,
|
|
||||||
anchors: &[(11, h!("A")), (14, h!("B"))],
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
name: "introduce_older_heights_after_chain_extends",
|
|
||||||
initial_env_height: 50,
|
|
||||||
initial_cps: &[10, 15],
|
|
||||||
final_env_height: 100,
|
|
||||||
anchors: &[(11, h!("A")), (14, h!("B"))],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (i, t) in test_cases.into_iter().enumerate() {
|
|
||||||
println!("[{}] running test case: {}", i, t.name);
|
|
||||||
|
|
||||||
let env = TestEnv::new()?;
|
|
||||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
|
||||||
let client = Builder::new(base_url.as_str()).build_async()?;
|
|
||||||
|
|
||||||
// set env to `initial_env_height`
|
|
||||||
if let Some(to_mine) = t
|
|
||||||
.initial_env_height
|
|
||||||
.checked_sub(env.make_checkpoint_tip().height())
|
|
||||||
{
|
|
||||||
env.mine_blocks(to_mine as _, None)?;
|
|
||||||
}
|
|
||||||
while client.get_height().await? < t.initial_env_height {
|
|
||||||
std::thread::sleep(Duration::from_millis(10));
|
|
||||||
}
|
|
||||||
|
|
||||||
// craft initial `local_chain`
|
|
||||||
let local_chain = {
|
|
||||||
let (mut chain, _) = LocalChain::from_genesis_hash(env.genesis_hash()?);
|
|
||||||
let chain_tip = chain.tip();
|
|
||||||
let update_blocks = bdk_esplora::init_chain_update(&client, &chain_tip).await?;
|
|
||||||
let update_anchors = t
|
|
||||||
.initial_cps
|
|
||||||
.iter()
|
|
||||||
.map(|&height| -> anyhow::Result<_> {
|
|
||||||
Ok((
|
|
||||||
BlockId {
|
|
||||||
height,
|
|
||||||
hash: env.bitcoind.client.get_block_hash(height as _)?,
|
|
||||||
},
|
|
||||||
Txid::all_zeros(),
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect::<anyhow::Result<BTreeSet<_>>>()?;
|
|
||||||
let chain_update = bdk_esplora::finalize_chain_update(
|
|
||||||
&client,
|
|
||||||
&chain_tip,
|
|
||||||
&update_anchors,
|
|
||||||
update_blocks,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
chain.apply_update(chain_update)?;
|
|
||||||
chain
|
|
||||||
};
|
|
||||||
println!("local chain height: {}", local_chain.tip().height());
|
|
||||||
|
|
||||||
// extend env chain
|
|
||||||
if let Some(to_mine) = t
|
|
||||||
.final_env_height
|
|
||||||
.checked_sub(env.make_checkpoint_tip().height())
|
|
||||||
{
|
|
||||||
env.mine_blocks(to_mine as _, None)?;
|
|
||||||
}
|
|
||||||
while client.get_height().await? < t.final_env_height {
|
|
||||||
std::thread::sleep(Duration::from_millis(10));
|
|
||||||
}
|
|
||||||
|
|
||||||
// craft update
|
|
||||||
let update = {
|
|
||||||
let local_tip = local_chain.tip();
|
|
||||||
let update_blocks = bdk_esplora::init_chain_update(&client, &local_tip).await?;
|
|
||||||
let update_anchors = t
|
|
||||||
.anchors
|
|
||||||
.iter()
|
|
||||||
.map(|&(height, txid)| -> anyhow::Result<_> {
|
|
||||||
Ok((
|
|
||||||
BlockId {
|
|
||||||
height,
|
|
||||||
hash: env.bitcoind.client.get_block_hash(height as _)?,
|
|
||||||
},
|
|
||||||
txid,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect::<anyhow::Result<_>>()?;
|
|
||||||
bdk_esplora::finalize_chain_update(&client, &local_tip, &update_anchors, update_blocks)
|
|
||||||
.await?
|
|
||||||
};
|
|
||||||
|
|
||||||
// apply update
|
|
||||||
let mut updated_local_chain = local_chain.clone();
|
|
||||||
updated_local_chain.apply_update(update)?;
|
|
||||||
println!(
|
|
||||||
"updated local chain height: {}",
|
|
||||||
updated_local_chain.tip().height()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
{
|
|
||||||
let initial_heights = local_chain
|
|
||||||
.iter_checkpoints()
|
|
||||||
.map(|cp| cp.height())
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
let updated_heights = updated_local_chain
|
|
||||||
.iter_checkpoints()
|
|
||||||
.map(|cp| cp.height())
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
updated_heights.is_superset(&initial_heights)
|
|
||||||
},
|
|
||||||
"heights from the initial chain must all be in the updated chain",
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
{
|
|
||||||
let exp_anchor_heights = t
|
|
||||||
.anchors
|
|
||||||
.iter()
|
|
||||||
.map(|(h, _)| *h)
|
|
||||||
.chain(t.initial_cps.iter().copied())
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
let anchor_heights = updated_local_chain
|
|
||||||
.iter_checkpoints()
|
|
||||||
.map(|cp| cp.height())
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
anchor_heights.is_superset(&exp_anchor_heights)
|
|
||||||
},
|
|
||||||
"anchor heights must all be in updated chain",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
use bdk_chain::bitcoin::hashes::Hash;
|
|
||||||
use bdk_chain::local_chain::LocalChain;
|
|
||||||
use bdk_chain::BlockId;
|
|
||||||
use bdk_esplora::EsploraExt;
|
use bdk_esplora::EsploraExt;
|
||||||
use electrsd::bitcoind::anyhow;
|
use electrsd::bitcoind::anyhow;
|
||||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||||
use esplora_client::{self, BlockHash, Builder};
|
use esplora_client::{self, Builder};
|
||||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
@ -13,187 +10,6 @@ use std::time::Duration;
|
|||||||
use bdk_chain::bitcoin::{Address, Amount, Txid};
|
use bdk_chain::bitcoin::{Address, Amount, Txid};
|
||||||
use bdk_testenv::TestEnv;
|
use bdk_testenv::TestEnv;
|
||||||
|
|
||||||
macro_rules! h {
|
|
||||||
($index:literal) => {{
|
|
||||||
bdk_chain::bitcoin::hashes::Hash::hash($index.as_bytes())
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! local_chain {
|
|
||||||
[ $(($height:expr, $block_hash:expr)), * ] => {{
|
|
||||||
#[allow(unused_mut)]
|
|
||||||
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*].into_iter().collect())
|
|
||||||
.expect("chain must have genesis block")
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensure that update does not remove heights (from original), and all anchor heights are included.
|
|
||||||
#[test]
|
|
||||||
pub fn test_finalize_chain_update() -> anyhow::Result<()> {
|
|
||||||
struct TestCase<'a> {
|
|
||||||
name: &'a str,
|
|
||||||
/// Initial blockchain height to start the env with.
|
|
||||||
initial_env_height: u32,
|
|
||||||
/// Initial checkpoint heights to start with.
|
|
||||||
initial_cps: &'a [u32],
|
|
||||||
/// The final blockchain height of the env.
|
|
||||||
final_env_height: u32,
|
|
||||||
/// The anchors to test with: `(height, txid)`. Only the height is provided as we can fetch
|
|
||||||
/// the blockhash from the env.
|
|
||||||
anchors: &'a [(u32, Txid)],
|
|
||||||
}
|
|
||||||
|
|
||||||
let test_cases = [
|
|
||||||
TestCase {
|
|
||||||
name: "chain_extends",
|
|
||||||
initial_env_height: 60,
|
|
||||||
initial_cps: &[59, 60],
|
|
||||||
final_env_height: 90,
|
|
||||||
anchors: &[],
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
name: "introduce_older_heights",
|
|
||||||
initial_env_height: 50,
|
|
||||||
initial_cps: &[10, 15],
|
|
||||||
final_env_height: 50,
|
|
||||||
anchors: &[(11, h!("A")), (14, h!("B"))],
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
name: "introduce_older_heights_after_chain_extends",
|
|
||||||
initial_env_height: 50,
|
|
||||||
initial_cps: &[10, 15],
|
|
||||||
final_env_height: 100,
|
|
||||||
anchors: &[(11, h!("A")), (14, h!("B"))],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (i, t) in test_cases.into_iter().enumerate() {
|
|
||||||
println!("[{}] running test case: {}", i, t.name);
|
|
||||||
|
|
||||||
let env = TestEnv::new()?;
|
|
||||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
|
||||||
let client = Builder::new(base_url.as_str()).build_blocking()?;
|
|
||||||
|
|
||||||
// set env to `initial_env_height`
|
|
||||||
if let Some(to_mine) = t
|
|
||||||
.initial_env_height
|
|
||||||
.checked_sub(env.make_checkpoint_tip().height())
|
|
||||||
{
|
|
||||||
env.mine_blocks(to_mine as _, None)?;
|
|
||||||
}
|
|
||||||
while client.get_height()? < t.initial_env_height {
|
|
||||||
std::thread::sleep(Duration::from_millis(10));
|
|
||||||
}
|
|
||||||
|
|
||||||
// craft initial `local_chain`
|
|
||||||
let local_chain = {
|
|
||||||
let (mut chain, _) = LocalChain::from_genesis_hash(env.genesis_hash()?);
|
|
||||||
let chain_tip = chain.tip();
|
|
||||||
let update_blocks = bdk_esplora::init_chain_update_blocking(&client, &chain_tip)?;
|
|
||||||
let update_anchors = t
|
|
||||||
.initial_cps
|
|
||||||
.iter()
|
|
||||||
.map(|&height| -> anyhow::Result<_> {
|
|
||||||
Ok((
|
|
||||||
BlockId {
|
|
||||||
height,
|
|
||||||
hash: env.bitcoind.client.get_block_hash(height as _)?,
|
|
||||||
},
|
|
||||||
Txid::all_zeros(),
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect::<anyhow::Result<BTreeSet<_>>>()?;
|
|
||||||
let chain_update = bdk_esplora::finalize_chain_update_blocking(
|
|
||||||
&client,
|
|
||||||
&chain_tip,
|
|
||||||
&update_anchors,
|
|
||||||
update_blocks,
|
|
||||||
)?;
|
|
||||||
chain.apply_update(chain_update)?;
|
|
||||||
chain
|
|
||||||
};
|
|
||||||
println!("local chain height: {}", local_chain.tip().height());
|
|
||||||
|
|
||||||
// extend env chain
|
|
||||||
if let Some(to_mine) = t
|
|
||||||
.final_env_height
|
|
||||||
.checked_sub(env.make_checkpoint_tip().height())
|
|
||||||
{
|
|
||||||
env.mine_blocks(to_mine as _, None)?;
|
|
||||||
}
|
|
||||||
while client.get_height()? < t.final_env_height {
|
|
||||||
std::thread::sleep(Duration::from_millis(10));
|
|
||||||
}
|
|
||||||
|
|
||||||
// craft update
|
|
||||||
let update = {
|
|
||||||
let local_tip = local_chain.tip();
|
|
||||||
let update_blocks = bdk_esplora::init_chain_update_blocking(&client, &local_tip)?;
|
|
||||||
let update_anchors = t
|
|
||||||
.anchors
|
|
||||||
.iter()
|
|
||||||
.map(|&(height, txid)| -> anyhow::Result<_> {
|
|
||||||
Ok((
|
|
||||||
BlockId {
|
|
||||||
height,
|
|
||||||
hash: env.bitcoind.client.get_block_hash(height as _)?,
|
|
||||||
},
|
|
||||||
txid,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect::<anyhow::Result<_>>()?;
|
|
||||||
bdk_esplora::finalize_chain_update_blocking(
|
|
||||||
&client,
|
|
||||||
&local_tip,
|
|
||||||
&update_anchors,
|
|
||||||
update_blocks,
|
|
||||||
)?
|
|
||||||
};
|
|
||||||
|
|
||||||
// apply update
|
|
||||||
let mut updated_local_chain = local_chain.clone();
|
|
||||||
updated_local_chain.apply_update(update)?;
|
|
||||||
println!(
|
|
||||||
"updated local chain height: {}",
|
|
||||||
updated_local_chain.tip().height()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
{
|
|
||||||
let initial_heights = local_chain
|
|
||||||
.iter_checkpoints()
|
|
||||||
.map(|cp| cp.height())
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
let updated_heights = updated_local_chain
|
|
||||||
.iter_checkpoints()
|
|
||||||
.map(|cp| cp.height())
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
updated_heights.is_superset(&initial_heights)
|
|
||||||
},
|
|
||||||
"heights from the initial chain must all be in the updated chain",
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
{
|
|
||||||
let exp_anchor_heights = t
|
|
||||||
.anchors
|
|
||||||
.iter()
|
|
||||||
.map(|(h, _)| *h)
|
|
||||||
.chain(t.initial_cps.iter().copied())
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
let anchor_heights = updated_local_chain
|
|
||||||
.iter_checkpoints()
|
|
||||||
.map(|cp| cp.height())
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
anchor_heights.is_superset(&exp_anchor_heights)
|
|
||||||
},
|
|
||||||
"anchor heights must all be in updated chain",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
@ -399,206 +215,3 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn update_local_chain() -> anyhow::Result<()> {
|
|
||||||
const TIP_HEIGHT: u32 = 50;
|
|
||||||
|
|
||||||
let env = TestEnv::new()?;
|
|
||||||
let blocks = {
|
|
||||||
let bitcoind_client = &env.bitcoind.client;
|
|
||||||
assert_eq!(bitcoind_client.get_block_count()?, 1);
|
|
||||||
[
|
|
||||||
(0, bitcoind_client.get_block_hash(0)?),
|
|
||||||
(1, bitcoind_client.get_block_hash(1)?),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.chain((2..).zip(env.mine_blocks((TIP_HEIGHT - 1) as usize, None)?))
|
|
||||||
.collect::<BTreeMap<_, _>>()
|
|
||||||
};
|
|
||||||
// so new blocks can be seen by Electrs
|
|
||||||
let env = env.reset_electrsd()?;
|
|
||||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
|
||||||
let client = Builder::new(base_url.as_str()).build_blocking();
|
|
||||||
|
|
||||||
struct TestCase {
|
|
||||||
name: &'static str,
|
|
||||||
chain: LocalChain,
|
|
||||||
request_heights: &'static [u32],
|
|
||||||
exp_update_heights: &'static [u32],
|
|
||||||
}
|
|
||||||
|
|
||||||
let test_cases = [
|
|
||||||
TestCase {
|
|
||||||
name: "request_later_blocks",
|
|
||||||
chain: local_chain![(0, blocks[&0]), (21, blocks[&21])],
|
|
||||||
request_heights: &[22, 25, 28],
|
|
||||||
exp_update_heights: &[21, 22, 25, 28],
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
name: "request_prev_blocks",
|
|
||||||
chain: local_chain![(0, blocks[&0]), (1, blocks[&1]), (5, blocks[&5])],
|
|
||||||
request_heights: &[4],
|
|
||||||
exp_update_heights: &[4, 5],
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
name: "request_prev_blocks_2",
|
|
||||||
chain: local_chain![(0, blocks[&0]), (1, blocks[&1]), (10, blocks[&10])],
|
|
||||||
request_heights: &[4, 6],
|
|
||||||
exp_update_heights: &[4, 6, 10],
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
name: "request_later_and_prev_blocks",
|
|
||||||
chain: local_chain![(0, blocks[&0]), (7, blocks[&7]), (11, blocks[&11])],
|
|
||||||
request_heights: &[8, 9, 15],
|
|
||||||
exp_update_heights: &[8, 9, 11, 15],
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
name: "request_tip_only",
|
|
||||||
chain: local_chain![(0, blocks[&0]), (5, blocks[&5]), (49, blocks[&49])],
|
|
||||||
request_heights: &[TIP_HEIGHT],
|
|
||||||
exp_update_heights: &[49],
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
name: "request_nothing",
|
|
||||||
chain: local_chain![(0, blocks[&0]), (13, blocks[&13]), (23, blocks[&23])],
|
|
||||||
request_heights: &[],
|
|
||||||
exp_update_heights: &[23],
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
name: "request_nothing_during_reorg",
|
|
||||||
chain: local_chain![(0, blocks[&0]), (13, blocks[&13]), (23, h!("23"))],
|
|
||||||
request_heights: &[],
|
|
||||||
exp_update_heights: &[13, 23],
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
name: "request_nothing_during_reorg_2",
|
|
||||||
chain: local_chain![
|
|
||||||
(0, blocks[&0]),
|
|
||||||
(21, blocks[&21]),
|
|
||||||
(22, h!("22")),
|
|
||||||
(23, h!("23"))
|
|
||||||
],
|
|
||||||
request_heights: &[],
|
|
||||||
exp_update_heights: &[21, 22, 23],
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
name: "request_prev_blocks_during_reorg",
|
|
||||||
chain: local_chain![
|
|
||||||
(0, blocks[&0]),
|
|
||||||
(21, blocks[&21]),
|
|
||||||
(22, h!("22")),
|
|
||||||
(23, h!("23"))
|
|
||||||
],
|
|
||||||
request_heights: &[17, 20],
|
|
||||||
exp_update_heights: &[17, 20, 21, 22, 23],
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
name: "request_later_blocks_during_reorg",
|
|
||||||
chain: local_chain![
|
|
||||||
(0, blocks[&0]),
|
|
||||||
(9, blocks[&9]),
|
|
||||||
(22, h!("22")),
|
|
||||||
(23, h!("23"))
|
|
||||||
],
|
|
||||||
request_heights: &[25, 27],
|
|
||||||
exp_update_heights: &[9, 22, 23, 25, 27],
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
name: "request_later_blocks_during_reorg_2",
|
|
||||||
chain: local_chain![(0, blocks[&0]), (9, h!("9"))],
|
|
||||||
request_heights: &[10],
|
|
||||||
exp_update_heights: &[0, 9, 10],
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
name: "request_later_and_prev_blocks_during_reorg",
|
|
||||||
chain: local_chain![(0, blocks[&0]), (1, blocks[&1]), (9, h!("9"))],
|
|
||||||
request_heights: &[8, 11],
|
|
||||||
exp_update_heights: &[1, 8, 9, 11],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (i, t) in test_cases.into_iter().enumerate() {
|
|
||||||
println!("Case {}: {}", i, t.name);
|
|
||||||
let mut chain = t.chain;
|
|
||||||
let cp_tip = chain.tip();
|
|
||||||
|
|
||||||
let new_blocks =
|
|
||||||
bdk_esplora::init_chain_update_blocking(&client, &cp_tip).map_err(|err| {
|
|
||||||
anyhow::format_err!("[{}:{}] `init_chain_update` failed: {}", i, t.name, err)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mock_anchors = t
|
|
||||||
.request_heights
|
|
||||||
.iter()
|
|
||||||
.map(|&h| {
|
|
||||||
let anchor_blockhash: BlockHash = bdk_chain::bitcoin::hashes::Hash::hash(
|
|
||||||
&format!("hash_at_height_{}", h).into_bytes(),
|
|
||||||
);
|
|
||||||
let txid: Txid = bdk_chain::bitcoin::hashes::Hash::hash(
|
|
||||||
&format!("txid_at_height_{}", h).into_bytes(),
|
|
||||||
);
|
|
||||||
let anchor = BlockId {
|
|
||||||
height: h,
|
|
||||||
hash: anchor_blockhash,
|
|
||||||
};
|
|
||||||
(anchor, txid)
|
|
||||||
})
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
|
|
||||||
let chain_update = bdk_esplora::finalize_chain_update_blocking(
|
|
||||||
&client,
|
|
||||||
&cp_tip,
|
|
||||||
&mock_anchors,
|
|
||||||
new_blocks,
|
|
||||||
)?;
|
|
||||||
let update_blocks = chain_update
|
|
||||||
.tip
|
|
||||||
.iter()
|
|
||||||
.map(|cp| cp.block_id())
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
|
|
||||||
let exp_update_blocks = t
|
|
||||||
.exp_update_heights
|
|
||||||
.iter()
|
|
||||||
.map(|&height| {
|
|
||||||
let hash = blocks[&height];
|
|
||||||
BlockId { height, hash }
|
|
||||||
})
|
|
||||||
.chain(
|
|
||||||
// Electrs Esplora `get_block` call fetches 10 blocks which is included in the
|
|
||||||
// update
|
|
||||||
blocks
|
|
||||||
.range(TIP_HEIGHT - 9..)
|
|
||||||
.map(|(&height, &hash)| BlockId { height, hash }),
|
|
||||||
)
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
update_blocks.is_superset(&exp_update_blocks),
|
|
||||||
"[{}:{}] unexpected update",
|
|
||||||
i,
|
|
||||||
t.name
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = chain
|
|
||||||
.apply_update(chain_update)
|
|
||||||
.unwrap_or_else(|err| panic!("[{}:{}] update failed to apply: {}", i, t.name, err));
|
|
||||||
|
|
||||||
// all requested heights must exist in the final chain
|
|
||||||
for height in t.request_heights {
|
|
||||||
let exp_blockhash = blocks.get(height).expect("block must exist in bitcoind");
|
|
||||||
assert_eq!(
|
|
||||||
chain.get(*height).map(|cp| cp.hash()),
|
|
||||||
Some(*exp_blockhash),
|
|
||||||
"[{}:{}] block {}:{} must exist in final chain",
|
|
||||||
i,
|
|
||||||
t.name,
|
|
||||||
height,
|
|
||||||
exp_blockhash
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user