]> Untitled Git - bdk/commitdiff
test(esplora): add `test_finalize_chain_update`
author志宇 <hello@evanlinjin.me>
Tue, 26 Mar 2024 12:29:20 +0000 (20:29 +0800)
committer志宇 <hello@evanlinjin.me>
Tue, 16 Apr 2024 10:01:51 +0000 (18:01 +0800)
We ensure that calling `finalize_chain_update` does not result in a
chain which removed previous heights and all anchor heights are
included.

crates/esplora/tests/async_ext.rs
crates/esplora/tests/blocking_ext.rs

index 5946bb4d84acdac7ca1fb9e6f96f377f5d50d227..e053ba72b844dd470ff9122b259e7a2cd2ffb34b 100644 (file)
@@ -1,3 +1,6 @@
+use bdk_chain::bitcoin::hashes::Hash;
+use bdk_chain::local_chain::LocalChain;
+use bdk_chain::BlockId;
 use bdk_esplora::EsploraAsyncExt;
 use electrsd::bitcoind::anyhow;
 use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
@@ -10,6 +13,175 @@ use std::time::Duration;
 use bdk_chain::bitcoin::{Address, Amount, Txid};
 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]
 pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
     let env = TestEnv::new()?;
index d35fab658870d1f6220c20b6c5b64cb4f4b06592..a9078d0312b5959ccaaa142e9a8ab74632a91dd3 100644 (file)
@@ -1,3 +1,4 @@
+use bdk_chain::bitcoin::hashes::Hash;
 use bdk_chain::local_chain::LocalChain;
 use bdk_chain::BlockId;
 use bdk_esplora::EsploraExt;
@@ -26,6 +27,173 @@ macro_rules! local_chain {
     }};
 }
 
+/// 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]
 pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
     let env = TestEnv::new()?;