Skip to content

Commit a6e613e

Browse files
committed
test(esplora): add test_finalize_chain_update
We ensure that calling `finalize_chain_update` does not result in a chain which removed previous heights and all anchor heights are included.
1 parent 494d253 commit a6e613e

File tree

2 files changed

+340
-0
lines changed

2 files changed

+340
-0
lines changed

crates/esplora/tests/async_ext.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use bdk_chain::bitcoin::hashes::Hash;
2+
use bdk_chain::local_chain::LocalChain;
3+
use bdk_chain::BlockId;
14
use bdk_esplora::EsploraAsyncExt;
25
use electrsd::bitcoind::anyhow;
36
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
@@ -10,6 +13,175 @@ use std::time::Duration;
1013
use bdk_chain::bitcoin::{Address, Amount, Txid};
1114
use bdk_testenv::TestEnv;
1215

16+
macro_rules! h {
17+
($index:literal) => {{
18+
bdk_chain::bitcoin::hashes::Hash::hash($index.as_bytes())
19+
}};
20+
}
21+
22+
/// Ensure that update does not remove heights (from original), and all anchor heights are included.
23+
#[tokio::test]
24+
pub async fn test_finalize_chain_update() -> anyhow::Result<()> {
25+
struct TestCase<'a> {
26+
name: &'a str,
27+
/// Initial blockchain height to start the env with.
28+
initial_env_height: u32,
29+
/// Initial checkpoint heights to start with.
30+
initial_cps: &'a [u32],
31+
/// The final blockchain height of the env.
32+
final_env_height: u32,
33+
/// The anchors to test with: `(height, txid)`. Only the height is provided as we can fetch
34+
/// the blockhash from the env.
35+
anchors: &'a [(u32, Txid)],
36+
}
37+
38+
let test_cases = [
39+
TestCase {
40+
name: "chain_extends",
41+
initial_env_height: 60,
42+
initial_cps: &[59, 60],
43+
final_env_height: 90,
44+
anchors: &[],
45+
},
46+
TestCase {
47+
name: "introduce_older_heights",
48+
initial_env_height: 50,
49+
initial_cps: &[10, 15],
50+
final_env_height: 50,
51+
anchors: &[(11, h!("A")), (14, h!("B"))],
52+
},
53+
TestCase {
54+
name: "introduce_older_heights_after_chain_extends",
55+
initial_env_height: 50,
56+
initial_cps: &[10, 15],
57+
final_env_height: 100,
58+
anchors: &[(11, h!("A")), (14, h!("B"))],
59+
},
60+
];
61+
62+
for (i, t) in test_cases.into_iter().enumerate() {
63+
println!("[{}] running test case: {}", i, t.name);
64+
65+
let env = TestEnv::new()?;
66+
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
67+
let client = Builder::new(base_url.as_str()).build_async()?;
68+
69+
// set env to `initial_env_height`
70+
if let Some(to_mine) = t
71+
.initial_env_height
72+
.checked_sub(env.make_checkpoint_tip().height())
73+
{
74+
env.mine_blocks(to_mine as _, None)?;
75+
}
76+
while client.get_height().await? < t.initial_env_height {
77+
std::thread::sleep(Duration::from_millis(10));
78+
}
79+
80+
// craft initial `local_chain`
81+
let local_chain = {
82+
let (mut chain, _) = LocalChain::from_genesis_hash(env.genesis_hash()?);
83+
let chain_tip = chain.tip();
84+
let update_blocks = bdk_esplora::init_chain_update(&client, &chain_tip).await?;
85+
let update_anchors = t
86+
.initial_cps
87+
.iter()
88+
.map(|&height| -> anyhow::Result<_> {
89+
Ok((
90+
BlockId {
91+
height,
92+
hash: env.bitcoind.client.get_block_hash(height as _)?,
93+
},
94+
Txid::all_zeros(),
95+
))
96+
})
97+
.collect::<anyhow::Result<BTreeSet<_>>>()?;
98+
let chain_update = bdk_esplora::finalize_chain_update(
99+
&client,
100+
&chain_tip,
101+
&update_anchors,
102+
update_blocks,
103+
)
104+
.await?;
105+
chain.apply_update(chain_update)?;
106+
chain
107+
};
108+
println!("local chain height: {}", local_chain.tip().height());
109+
110+
// extend env chain
111+
if let Some(to_mine) = t
112+
.final_env_height
113+
.checked_sub(env.make_checkpoint_tip().height())
114+
{
115+
env.mine_blocks(to_mine as _, None)?;
116+
}
117+
while client.get_height().await? < t.final_env_height {
118+
std::thread::sleep(Duration::from_millis(10));
119+
}
120+
121+
// craft update
122+
let update = {
123+
let local_tip = local_chain.tip();
124+
let update_blocks = bdk_esplora::init_chain_update(&client, &local_tip).await?;
125+
let update_anchors = t
126+
.anchors
127+
.iter()
128+
.map(|&(height, txid)| -> anyhow::Result<_> {
129+
Ok((
130+
BlockId {
131+
height,
132+
hash: env.bitcoind.client.get_block_hash(height as _)?,
133+
},
134+
txid,
135+
))
136+
})
137+
.collect::<anyhow::Result<_>>()?;
138+
bdk_esplora::finalize_chain_update(&client, &local_tip, &update_anchors, update_blocks)
139+
.await?
140+
};
141+
142+
// apply update
143+
let mut updated_local_chain = local_chain.clone();
144+
updated_local_chain.apply_update(update)?;
145+
println!(
146+
"updated local chain height: {}",
147+
updated_local_chain.tip().height()
148+
);
149+
150+
assert!(
151+
{
152+
let initial_heights = local_chain
153+
.iter_checkpoints()
154+
.map(|cp| cp.height())
155+
.collect::<BTreeSet<_>>();
156+
let updated_heights = updated_local_chain
157+
.iter_checkpoints()
158+
.map(|cp| cp.height())
159+
.collect::<BTreeSet<_>>();
160+
updated_heights.is_superset(&initial_heights)
161+
},
162+
"heights from the initial chain must all be in the updated chain",
163+
);
164+
165+
assert!(
166+
{
167+
let exp_anchor_heights = t
168+
.anchors
169+
.iter()
170+
.map(|(h, _)| *h)
171+
.chain(t.initial_cps.iter().copied())
172+
.collect::<BTreeSet<_>>();
173+
let anchor_heights = updated_local_chain
174+
.iter_checkpoints()
175+
.map(|cp| cp.height())
176+
.collect::<BTreeSet<_>>();
177+
anchor_heights.is_superset(&exp_anchor_heights)
178+
},
179+
"anchor heights must all be in updated chain",
180+
);
181+
}
182+
183+
Ok(())
184+
}
13185
#[tokio::test]
14186
pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
15187
let env = TestEnv::new()?;

crates/esplora/tests/blocking_ext.rs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use bdk_chain::bitcoin::hashes::Hash;
12
use bdk_chain::local_chain::LocalChain;
23
use bdk_chain::BlockId;
34
use bdk_esplora::EsploraExt;
@@ -26,6 +27,173 @@ macro_rules! local_chain {
2627
}};
2728
}
2829

30+
/// Ensure that update does not remove heights (from original), and all anchor heights are included.
31+
#[test]
32+
pub fn test_finalize_chain_update() -> anyhow::Result<()> {
33+
struct TestCase<'a> {
34+
name: &'a str,
35+
/// Initial blockchain height to start the env with.
36+
initial_env_height: u32,
37+
/// Initial checkpoint heights to start with.
38+
initial_cps: &'a [u32],
39+
/// The final blockchain height of the env.
40+
final_env_height: u32,
41+
/// The anchors to test with: `(height, txid)`. Only the height is provided as we can fetch
42+
/// the blockhash from the env.
43+
anchors: &'a [(u32, Txid)],
44+
}
45+
46+
let test_cases = [
47+
TestCase {
48+
name: "chain_extends",
49+
initial_env_height: 60,
50+
initial_cps: &[59, 60],
51+
final_env_height: 90,
52+
anchors: &[],
53+
},
54+
TestCase {
55+
name: "introduce_older_heights",
56+
initial_env_height: 50,
57+
initial_cps: &[10, 15],
58+
final_env_height: 50,
59+
anchors: &[(11, h!("A")), (14, h!("B"))],
60+
},
61+
TestCase {
62+
name: "introduce_older_heights_after_chain_extends",
63+
initial_env_height: 50,
64+
initial_cps: &[10, 15],
65+
final_env_height: 100,
66+
anchors: &[(11, h!("A")), (14, h!("B"))],
67+
},
68+
];
69+
70+
for (i, t) in test_cases.into_iter().enumerate() {
71+
println!("[{}] running test case: {}", i, t.name);
72+
73+
let env = TestEnv::new()?;
74+
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
75+
let client = Builder::new(base_url.as_str()).build_blocking()?;
76+
77+
// set env to `initial_env_height`
78+
if let Some(to_mine) = t
79+
.initial_env_height
80+
.checked_sub(env.make_checkpoint_tip().height())
81+
{
82+
env.mine_blocks(to_mine as _, None)?;
83+
}
84+
while client.get_height()? < t.initial_env_height {
85+
std::thread::sleep(Duration::from_millis(10));
86+
}
87+
88+
// craft initial `local_chain`
89+
let local_chain = {
90+
let (mut chain, _) = LocalChain::from_genesis_hash(env.genesis_hash()?);
91+
let chain_tip = chain.tip();
92+
let update_blocks = bdk_esplora::init_chain_update_blocking(&client, &chain_tip)?;
93+
let update_anchors = t
94+
.initial_cps
95+
.iter()
96+
.map(|&height| -> anyhow::Result<_> {
97+
Ok((
98+
BlockId {
99+
height,
100+
hash: env.bitcoind.client.get_block_hash(height as _)?,
101+
},
102+
Txid::all_zeros(),
103+
))
104+
})
105+
.collect::<anyhow::Result<BTreeSet<_>>>()?;
106+
let chain_update = bdk_esplora::finalize_chain_update_blocking(
107+
&client,
108+
&chain_tip,
109+
&update_anchors,
110+
update_blocks,
111+
)?;
112+
chain.apply_update(chain_update)?;
113+
chain
114+
};
115+
println!("local chain height: {}", local_chain.tip().height());
116+
117+
// extend env chain
118+
if let Some(to_mine) = t
119+
.final_env_height
120+
.checked_sub(env.make_checkpoint_tip().height())
121+
{
122+
env.mine_blocks(to_mine as _, None)?;
123+
}
124+
while client.get_height()? < t.final_env_height {
125+
std::thread::sleep(Duration::from_millis(10));
126+
}
127+
128+
// craft update
129+
let update = {
130+
let local_tip = local_chain.tip();
131+
let update_blocks = bdk_esplora::init_chain_update_blocking(&client, &local_tip)?;
132+
let update_anchors = t
133+
.anchors
134+
.iter()
135+
.map(|&(height, txid)| -> anyhow::Result<_> {
136+
Ok((
137+
BlockId {
138+
height,
139+
hash: env.bitcoind.client.get_block_hash(height as _)?,
140+
},
141+
txid,
142+
))
143+
})
144+
.collect::<anyhow::Result<_>>()?;
145+
bdk_esplora::finalize_chain_update_blocking(
146+
&client,
147+
&local_tip,
148+
&update_anchors,
149+
update_blocks,
150+
)?
151+
};
152+
153+
// apply update
154+
let mut updated_local_chain = local_chain.clone();
155+
updated_local_chain.apply_update(update)?;
156+
println!(
157+
"updated local chain height: {}",
158+
updated_local_chain.tip().height()
159+
);
160+
161+
assert!(
162+
{
163+
let initial_heights = local_chain
164+
.iter_checkpoints()
165+
.map(|cp| cp.height())
166+
.collect::<BTreeSet<_>>();
167+
let updated_heights = updated_local_chain
168+
.iter_checkpoints()
169+
.map(|cp| cp.height())
170+
.collect::<BTreeSet<_>>();
171+
updated_heights.is_superset(&initial_heights)
172+
},
173+
"heights from the initial chain must all be in the updated chain",
174+
);
175+
176+
assert!(
177+
{
178+
let exp_anchor_heights = t
179+
.anchors
180+
.iter()
181+
.map(|(h, _)| *h)
182+
.chain(t.initial_cps.iter().copied())
183+
.collect::<BTreeSet<_>>();
184+
let anchor_heights = updated_local_chain
185+
.iter_checkpoints()
186+
.map(|cp| cp.height())
187+
.collect::<BTreeSet<_>>();
188+
anchor_heights.is_superset(&exp_anchor_heights)
189+
},
190+
"anchor heights must all be in updated chain",
191+
);
192+
}
193+
194+
Ok(())
195+
}
196+
29197
#[test]
30198
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
31199
let env = TestEnv::new()?;

0 commit comments

Comments
 (0)