Skip to content

Commit 89872cd

Browse files
write scanning test for bdk_electrum
1 parent 5a090fa commit 89872cd

File tree

2 files changed

+383
-0
lines changed

2 files changed

+383
-0
lines changed

crates/electrum/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,7 @@ readme = "README.md"
1414
[dependencies]
1515
bdk_chain = { path = "../chain", version = "0.4.0", features = ["serde", "miniscript"] }
1616
electrum-client = { version = "0.12" }
17+
18+
[dev-dependencies]
19+
anyhow = "1"
20+
electrsd = { version = "0.22.0", features = ["legacy", "bitcoind_22_0"] }
Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
use bdk_chain::{ keychain::KeychainTxOutIndex, collections::BTreeMap, bitcoin::{ BlockHash, OutPoint, Txid, hashes::Hash, Transaction}, miniscript::{Descriptor, DescriptorPublicKey}, bitcoin::{Amount, Address, Network::Regtest, Script}, sparse_chain::ChainPosition, TxHeight, chain_graph::ChainGraph};
2+
use bdk_electrum::{ ElectrumExt };
3+
use electrsd::{ ElectrsD, bitcoind::{self, BitcoinD, bitcoincore_rpc::{bitcoincore_rpc_json::AddressType, RpcApi}}, electrum_client::ElectrumApi};
4+
use electrum_client::{ Client };
5+
use std::{ env, time::{ Duration, Instant } };
6+
use anyhow::Result;
7+
8+
#[derive(Debug, Clone, PartialOrd, PartialEq, Ord, Eq)]
9+
enum Keychain {
10+
External,
11+
Internal
12+
}
13+
14+
#[test]
15+
fn test_scanning_stop_gap() -> Result<()> {
16+
let (bitcoin_daemon, electrs_daemon, tcp_client) = init_test_tools(None, None);
17+
premine(&bitcoin_daemon, &electrs_daemon, 101);
18+
19+
20+
let local_chain:BTreeMap<u32, BlockHash> = BTreeMap::new();
21+
let mut txout_index = init_txout_index();
22+
let chain_graph = ChainGraph::default();
23+
24+
25+
let (tx, revealed_spks) = send_to_revealed_script(&mut txout_index, &bitcoin_daemon, 19, 19);
26+
let (_, to_script) = revealed_spks.get(19).unwrap().to_owned();
27+
28+
generate_blocks_and_wait(&bitcoin_daemon, &electrs_daemon, 1);
29+
30+
let mut spks = BTreeMap::new();
31+
spks.insert(Keychain::External, revealed_spks.into_iter());
32+
let electrum_update = tcp_client.scan(&local_chain, spks, [], [], 20, 5).unwrap();
33+
let new_txs = tcp_client.batch_transaction_get(electrum_update.missing_full_txs(&chain_graph))?;
34+
let keychain_scan = electrum_update.into_keychain_scan(new_txs, &chain_graph)?;
35+
println!("Last Active indices {:?}", keychain_scan.last_active_indices);
36+
assert_eq!(*keychain_scan.last_active_indices.get(&Keychain::External).unwrap(), 19);
37+
let (&conf, chain_tx) = keychain_scan.update.get_tx_in_chain(tx.txid()).unwrap();
38+
assert_eq!(tx, chain_tx.clone());
39+
assert!(conf.is_confirmed());
40+
assert_eq!(conf.height(), TxHeight::Confirmed(103));
41+
let (output_vout, _) = tx.output.iter().enumerate().find(|(_idx, out)| out.script_pubkey == to_script).unwrap();
42+
let full_txout = keychain_scan.update.full_txout(bdk_chain::bitcoin::OutPoint { txid: tx.txid(), vout: output_vout as u32 });
43+
assert_eq!(full_txout.unwrap().txout.value, 10000);
44+
45+
46+
let (tx, revealed_spks) = send_to_revealed_script(&mut txout_index, &bitcoin_daemon, 38, 18);
47+
let (_, to_script) = revealed_spks.get(18).unwrap().to_owned();
48+
49+
generate_blocks_and_wait(&bitcoin_daemon, &electrs_daemon, 1);
50+
51+
let mut spks = BTreeMap::new();
52+
spks.insert(Keychain::External, revealed_spks.into_iter());
53+
let electrum_update = tcp_client.scan(&local_chain, spks, [], [], 20, 5).unwrap();
54+
let new_txs = tcp_client.batch_transaction_get(electrum_update.missing_full_txs(&chain_graph))?;
55+
let keychain_scan = electrum_update.into_keychain_scan(new_txs, &chain_graph)?;
56+
println!("Last Active indices {:?}", keychain_scan.last_active_indices);
57+
assert_eq!(*keychain_scan.last_active_indices.get(&Keychain::External).unwrap(), 38);
58+
let (&conf, chain_tx) = keychain_scan.update.get_tx_in_chain(tx.txid()).unwrap();
59+
assert_eq!(tx, chain_tx.clone());
60+
assert!(conf.is_confirmed());
61+
assert_eq!(conf.height(), TxHeight::Confirmed(104));
62+
let (output_vout, _) = tx.output.iter().enumerate().find(|(_idx, out)| out.script_pubkey == to_script).unwrap();
63+
let full_txout = keychain_scan.update.full_txout(bdk_chain::bitcoin::OutPoint { txid: tx.txid(), vout: output_vout as u32 });
64+
assert_eq!(full_txout.unwrap().txout.value, 10000);
65+
66+
let (tx, revealed_spks) = send_to_revealed_script(&mut txout_index, &bitcoin_daemon, 59, 20);
67+
let (_, to_script) = revealed_spks.get(20).unwrap().to_owned();
68+
69+
generate_blocks_and_wait(&bitcoin_daemon, &electrs_daemon, 1);
70+
71+
let mut spks = BTreeMap::new();
72+
spks.insert(Keychain::External, revealed_spks.into_iter());
73+
let electrum_update = tcp_client.scan(&local_chain, spks, [], [], 20, 5).unwrap();
74+
println!("Last Active indices {:?}", keychain_scan.last_active_indices);
75+
assert!(keychain_scan.last_active_indices.get(&Keychain::External).is_none());
76+
assert!(keychain_scan.update.get_tx_in_chain(tx.txid()).is_none());
77+
let (output_vout, _) = tx.output.iter().enumerate().find(|(_idx, out)| out.script_pubkey == to_script).unwrap();
78+
assert!(keychain_scan.update.full_txout(bdk_chain::bitcoin::OutPoint { txid: tx.txid(), vout: output_vout as u32 }).is_none());
79+
80+
Ok(())
81+
82+
}
83+
84+
#[test]
85+
fn test_reorg() -> Result<()> {
86+
let mut conf = bitcoind::Conf::default();
87+
conf.p2p = bitcoind::P2P::Yes;
88+
let (bitcoin_daemon, electrs_daemon, tcp_client) = init_test_tools( Some(conf), None);
89+
let mut miner_conf = bitcoind::Conf::default();
90+
miner_conf.p2p = bitcoin_daemon.p2p_connect(true).unwrap();
91+
let miner_node = setup_bitcoind(Some(miner_conf));
92+
premine(&bitcoin_daemon, &electrs_daemon, 101);
93+
94+
let local_chain:BTreeMap<u32, BlockHash> = BTreeMap::new();
95+
let mut txout_index = init_txout_index();
96+
let chain_graph = ChainGraph::default();
97+
98+
let (tx, revealed_spks) = send_to_revealed_script(&mut txout_index, &bitcoin_daemon, 0, 0);
99+
let (_, revealed_spk) = revealed_spks.get(0).unwrap().to_owned();
100+
101+
102+
// Get the transaction confirmed above
103+
generate_blocks_and_wait(&bitcoin_daemon, &electrs_daemon, 1);
104+
105+
let mut spks = BTreeMap::new();
106+
spks.insert(Keychain::External, [(0, revealed_spk.clone())].into_iter());
107+
let electrum_update = tcp_client.scan(&local_chain, spks, [], [], 20, 5).unwrap();
108+
let new_txs = tcp_client.batch_transaction_get(electrum_update.missing_full_txs(&chain_graph))?;
109+
let keychain_scan = electrum_update.into_keychain_scan(new_txs, &chain_graph)?;
110+
assert_eq!(*keychain_scan.last_active_indices.get(&Keychain::External).unwrap(), 0);
111+
let (&conf, chain_tx) = keychain_scan.update.get_tx_in_chain(tx.txid()).unwrap();
112+
assert_eq!(tx, chain_tx.clone());
113+
assert!(conf.is_confirmed());
114+
assert_eq!(conf.height(), TxHeight::Confirmed(103));
115+
let (output_vout, _) = tx.output.iter().enumerate().find(|(_idx, out)| out.value == 10000).unwrap();
116+
let full_txout = keychain_scan.update.full_txout(bdk_chain::bitcoin::OutPoint { txid: tx.txid(), vout: output_vout as u32 });
117+
assert_eq!(full_txout.unwrap().txout.value, 10000);
118+
119+
// Reorg blocks on miner chain
120+
reorg(3, &miner_node)?;
121+
// Generate more blocks on the miner node, thereby making it the chain with the most
122+
// work, so the bitcoin_daemon chain has to catch up on this chain which doesn't
123+
// have a transaction above.
124+
generate_blocks_and_wait(&miner_node, &electrs_daemon, 5);
125+
126+
127+
let mut spks = BTreeMap::new();
128+
spks.insert(Keychain::External, [(0, revealed_spk.clone())].into_iter());
129+
let electrum_update = tcp_client.scan(&local_chain, spks, [], [], 20, 5).unwrap();
130+
let new_txs = tcp_client.batch_transaction_get(electrum_update.missing_full_txs(&chain_graph))?;
131+
let keychain_scan = electrum_update.into_keychain_scan(new_txs, &chain_graph)?;
132+
let (conf, _tx_chain) = keychain_scan.update.get_tx_in_chain(tx.txid()).unwrap();
133+
assert!(!conf.is_confirmed());
134+
let (output_vout, _) = tx.output.iter().enumerate().find(|(_idx, out)| out.value == 10000).unwrap();
135+
let full_txout = keychain_scan.update.full_txout(bdk_chain::bitcoin::OutPoint { txid: tx.txid(), vout: output_vout as u32 }).unwrap();
136+
assert!(!full_txout.chain_position.is_confirmed());
137+
138+
Ok(())
139+
140+
}
141+
142+
#[test]
143+
fn test_scan_with_txids() -> Result<()> {
144+
let (bitcoin_daemon, electrs_daemon, tcp_client) = init_test_tools(None, None);
145+
premine(&bitcoin_daemon, &electrs_daemon, 101);
146+
147+
let local_chain:BTreeMap<u32, BlockHash> = BTreeMap::new();
148+
let mut txout_index = init_txout_index();
149+
let chain_graph = ChainGraph::default();
150+
151+
let (tx_1, _) = send_to_revealed_script(&mut txout_index, &bitcoin_daemon, 0, 0);
152+
153+
generate_blocks_and_wait(&bitcoin_daemon, &electrs_daemon, 1);
154+
155+
let electrum_update = tcp_client.scan(&local_chain, BTreeMap::<Keychain, Vec<(u32, Script)>>::new(), [tx_1.txid()], [], 20, 5).unwrap();
156+
let new_txs = tcp_client.batch_transaction_get(electrum_update.missing_full_txs(&chain_graph))?;
157+
let keychain_scan = electrum_update.into_keychain_scan(new_txs, &chain_graph)?;
158+
assert!(keychain_scan.last_active_indices.get(&Keychain::External).is_none());
159+
let (conf, tx1_chain) = keychain_scan.update.get_tx_in_chain(tx_1.txid()).unwrap();
160+
assert_eq!(tx_1, tx1_chain.clone());
161+
assert!(conf.is_confirmed());
162+
assert_eq!(conf.height(), TxHeight::Confirmed(103));
163+
let checkpoint = keychain_scan.update.chain().checkpoint_at(103).unwrap();
164+
assert_eq!(checkpoint.hash, bitcoin_daemon.client.get_block_hash(103).unwrap());
165+
166+
let (tx_2, _) = send_to_revealed_script(&mut txout_index, &bitcoin_daemon, 1, 0);
167+
168+
wait_for_tx_appears_in_esplora(5, &electrs_daemon, &tx_2.txid());
169+
let electrum_update = tcp_client.scan(&local_chain, BTreeMap::<Keychain, Vec<(u32, Script)>>::new(), [tx_2.txid()], [], 20, 5).unwrap();
170+
let new_txs = tcp_client.batch_transaction_get(electrum_update.missing_full_txs(&chain_graph))?;
171+
let keychain_scan = electrum_update.into_keychain_scan(new_txs, &chain_graph)?;
172+
let (conf, _tx_chain) = keychain_scan.update.get_tx_in_chain(tx_2.txid()).unwrap();
173+
assert!(!conf.is_confirmed());
174+
175+
Ok(())
176+
}
177+
178+
#[test]
179+
fn test_scan_with_outpoints() -> Result<()> {
180+
let (bitcoin_daemon, electrs_daemon, tcp_client) = init_test_tools(None, None);
181+
premine(&bitcoin_daemon, &electrs_daemon, 101);
182+
183+
184+
let local_chain:BTreeMap<u32, BlockHash> = BTreeMap::new();
185+
let mut txout_index = init_txout_index();
186+
let chain_graph = ChainGraph::default();
187+
188+
let mut outpoints: [OutPoint; 2] = [OutPoint::null(), OutPoint::null()];
189+
let mut txids: [Txid; 2] = [Txid::from_inner([0x00; 32]), Txid::from_inner([0x00; 32])];
190+
for i in 0..=1 {
191+
let (tx, revealed_spks) = send_to_revealed_script(&mut txout_index, &bitcoin_daemon, i, 0);
192+
let (_, revealed_spk) = revealed_spks.get(0).unwrap().to_owned();
193+
let (output_vout, _) = tx.output.iter().enumerate().find(|(_idx, out)| out.script_pubkey == revealed_spk.clone()).unwrap();
194+
outpoints[i as usize] = OutPoint::new(tx.txid(), output_vout as u32);
195+
txids[i as usize] = tx.txid();
196+
}
197+
198+
let electrum_update = tcp_client.scan(&local_chain, BTreeMap::<Keychain, Vec<(u32, Script)>>::new(), [], outpoints, 20, 5).unwrap();
199+
let new_txs = tcp_client.batch_transaction_get(electrum_update.missing_full_txs(&chain_graph))?;
200+
let keychain_scan = electrum_update.into_keychain_scan(new_txs, &chain_graph)?;
201+
for i in 0..=1 {
202+
assert!(keychain_scan.update.get_tx_in_chain(txids[i]).is_none());
203+
assert!(keychain_scan.update.full_txout(outpoints[i]).is_none());
204+
}
205+
206+
generate_blocks_and_wait(&bitcoin_daemon, &electrs_daemon, 1);
207+
208+
let electrum_update = tcp_client.scan(&local_chain, BTreeMap::<Keychain, Vec<(u32, Script)>>::new(), [], outpoints, 20, 5).unwrap();
209+
let new_txs = tcp_client.batch_transaction_get(electrum_update.missing_full_txs(&chain_graph))?;
210+
let keychain_scan = electrum_update.into_keychain_scan(new_txs, &chain_graph)?;
211+
for i in 0..=1 {
212+
let (conf, tx) = keychain_scan.update.get_tx_in_chain(txids[i]).unwrap();
213+
assert!(conf.is_confirmed());
214+
assert_eq!(tx.txid(), txids[i]);
215+
let full_txout = keychain_scan.update.full_txout(outpoints[i]).unwrap();
216+
assert!(full_txout.chain_position.is_confirmed());
217+
}
218+
219+
Ok(())
220+
221+
}
222+
223+
fn init_txout_index() -> KeychainTxOutIndex::<Keychain> {
224+
let mut txout_index = KeychainTxOutIndex::<Keychain>::default();
225+
let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::default();
226+
let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
227+
let (internal_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap();
228+
229+
txout_index.add_keychain(Keychain::External, external_descriptor);
230+
txout_index.add_keychain(Keychain::Internal, internal_descriptor);
231+
txout_index
232+
}
233+
234+
fn init_test_tools(bitcoind_conf: Option<bitcoind::Conf>, electrsd_conf: Option<electrsd::Conf>) -> (BitcoinD, ElectrsD, Client){
235+
let bitcoin_daemon = setup_bitcoind(bitcoind_conf);
236+
let electrs_daemon = setup_electrsd(electrsd_conf, &bitcoin_daemon);
237+
let tcp_client = setup_client(&electrs_daemon);
238+
239+
(bitcoin_daemon, electrs_daemon, tcp_client)
240+
}
241+
242+
fn send_to_revealed_script(txout_index: &mut KeychainTxOutIndex<Keychain>, bitcoin_daemon: &BitcoinD, reveal_target: u32, last_revealed_idx: usize) -> (Transaction, Vec<(u32, Script)>) {
243+
let revealed_spks = txout_index.reveal_to_target(&Keychain::External, reveal_target).0.collect::<Vec<(u32, Script)>>();
244+
println!("Revealed Spks {:?}", revealed_spks);
245+
let (_idx, script) = revealed_spks.get(last_revealed_idx).unwrap().to_owned();
246+
let address = Address::from_script(&script, Regtest).unwrap();
247+
let txid = bitcoin_daemon
248+
.client
249+
.send_to_address(
250+
&address,
251+
Amount::from_sat(10000),
252+
None,
253+
None,
254+
None,
255+
None,
256+
None,
257+
None,
258+
)
259+
.unwrap();
260+
261+
(bitcoin_daemon.client.get_transaction(&txid, Some(false)).unwrap().transaction().unwrap(), revealed_spks)
262+
}
263+
264+
fn wait_for_block(electrs_daemon: &ElectrsD, min_height: usize) {
265+
let mut header = electrs_daemon.client.block_headers_subscribe().unwrap();
266+
loop {
267+
if header.height >= min_height {
268+
break;
269+
}
270+
header = exponential_backoff_poll(|| {
271+
electrs_daemon.trigger().unwrap();
272+
electrs_daemon.client.ping().unwrap();
273+
electrs_daemon.client.block_headers_pop().unwrap()
274+
});
275+
}
276+
}
277+
278+
fn wait_for_tx_appears_in_esplora(wait_seconds: u64, electrs_daemon: &ElectrsD, txid: &bdk_chain::bitcoin::Txid) -> bool {
279+
let instant = Instant::now();
280+
loop {
281+
let wait_tx = electrs_daemon.client.transaction_get(txid);
282+
if wait_tx.is_ok() {
283+
return true;
284+
}
285+
286+
if instant.elapsed() >= Duration::from_secs(wait_seconds) {
287+
return false;
288+
}
289+
}
290+
}
291+
292+
fn generate_blocks(bitcoin_daemon: &BitcoinD, num: usize) {
293+
let address = bitcoin_daemon
294+
.client
295+
.get_new_address(Some("test"), Some(AddressType::Bech32))
296+
.unwrap();
297+
let _block_hashes = bitcoin_daemon
298+
.client
299+
.generate_to_address(num as u64, &address)
300+
.unwrap();
301+
}
302+
303+
fn premine(bitcoin_daemon: &BitcoinD, electrs_daemon: &ElectrsD, num_blocks: usize) {
304+
generate_blocks_and_wait(bitcoin_daemon, electrs_daemon, num_blocks);
305+
}
306+
307+
fn generate_blocks_and_wait(bitcoin_daemon: &BitcoinD, electrs_daemon: &ElectrsD, num: usize) {
308+
let curr_height = bitcoin_daemon.client.get_block_count().unwrap();
309+
generate_blocks(bitcoin_daemon, num);
310+
wait_for_block(electrs_daemon, curr_height as usize + num);
311+
}
312+
313+
314+
fn reorg(num_blocks: usize, bitcoin_daemon: &BitcoinD) -> Result<()> {
315+
let best_hash = bitcoin_daemon.client.get_best_block_hash()?;
316+
let initial_height = bitcoin_daemon.client.get_block_info(&best_hash)?.height;
317+
318+
let mut to_invalidate = best_hash;
319+
for i in 1..=num_blocks {
320+
println!(
321+
"Invalidating block {}/{} ({})",
322+
i, num_blocks, to_invalidate
323+
);
324+
325+
bitcoin_daemon.client.invalidate_block(&to_invalidate)?;
326+
to_invalidate = bitcoin_daemon.client.get_best_block_hash()?;
327+
}
328+
329+
println!(
330+
"Invalidated {} blocks to new height of {}",
331+
num_blocks,
332+
initial_height - num_blocks as usize
333+
);
334+
335+
Ok(())
336+
}
337+
338+
339+
fn setup_client(electrs_daemon: &ElectrsD) -> Client {
340+
Client::new(electrs_daemon.electrum_url.as_str()).unwrap()
341+
}
342+
343+
fn setup_bitcoind(conf: Option<bitcoind::Conf>) -> BitcoinD {
344+
let bitcoind_exe = env::var("BITCOIND_EXE")
345+
.ok()
346+
.or_else(|| bitcoind::downloaded_exe_path().ok())
347+
.expect(
348+
"you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature",
349+
);
350+
let conf = conf.unwrap_or_else(|| bitcoind::Conf::default());
351+
352+
BitcoinD::with_conf(bitcoind_exe, &conf).unwrap()
353+
}
354+
355+
fn setup_electrsd(conf: Option<electrsd::Conf>, bitcoin_daemon: &BitcoinD) -> ElectrsD {
356+
let electrs_exe = env::var("ELECTRS_EXE")
357+
.ok()
358+
.or_else(electrsd::downloaded_exe_path)
359+
.expect("you need to provide env var ELECTRS_EXE or specify an electrsd version feature");
360+
let mut conf = conf.unwrap_or_else(|| electrsd::Conf::default());
361+
conf.http_enabled = true;
362+
ElectrsD::with_conf(electrs_exe, &bitcoin_daemon, &conf).unwrap()
363+
}
364+
365+
fn exponential_backoff_poll<T, F>(mut poll: F) -> T
366+
where
367+
F: FnMut() -> Option<T>,
368+
{
369+
let mut delay = Duration::from_millis(64);
370+
loop {
371+
match poll() {
372+
Some(data) => break data,
373+
None if delay.as_millis() < 512 => delay = delay.mul_f32(2.0),
374+
None => {}
375+
}
376+
377+
std::thread::sleep(delay);
378+
}
379+
}

0 commit comments

Comments
 (0)