Skip to content

Commit caef3f4

Browse files
committed
Merge #1535: test(electrum): Test sync in reorg and no-reorg situations
2c0bc45 feat(testenv): Added `bdk_electrum` wait for Txid method (Wei Chen) 49e1a5e test(electrum): Test sync in reorg and no-reorg situations (Wei Chen) Pull request description: <!-- You can erase any parts of this template not applicable to your Pull Request. --> Closes #1125. ### Description <!-- Describe the purpose of this PR, what's being adding and/or fixed --> Add new test for `bdk_electrum` to make sure previously unconfirmed transactions get confirmed again in both reorg and no-reorg situations. ### Changelog notice <!-- Notice the release manager should include in the release tag message changelog --> <!-- See https://keepachangelog.com/en/1.0.0/ for examples --> * Added `wait_until_electrum_sees_txid` method to `TestEnv`. * `wait_until_electrum_sees_block` now has a `Duration` input for timeout. * Removed exponential polling delay in `wait_until_electrum_sees_block`. * Added `test_sync` to `bdk_electrum` to make sure previously unconfirmed transactions get confirmed in both reorg and no-org situations. ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing ACKs for top commit: evanlinjin: ACK 2c0bc45 Tree-SHA512: 2218151267fe0a3dc3eefa1e7081902e25193f211b2624a3a9f2007b9b38eefb5b847754b9f8cbefba6be6a497cfbc85ac3e5a2db40f01fcc71a3c9d8427adf4
2 parents d58d75e + 2c0bc45 commit caef3f4

File tree

2 files changed

+160
-53
lines changed

2 files changed

+160
-53
lines changed

crates/electrum/tests/test_electrum.rs

Lines changed: 126 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
use bdk_chain::{
22
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash},
33
local_chain::LocalChain,
4-
spk_client::{FullScanRequest, SyncRequest},
4+
spk_client::{FullScanRequest, SyncRequest, SyncResult},
55
spk_txout::SpkTxOutIndex,
6-
Balance, ConfirmationBlockTime, IndexedTxGraph,
6+
Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge,
77
};
88
use bdk_electrum::BdkElectrumClient;
99
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
10+
use core::time::Duration;
1011
use std::collections::{BTreeSet, HashSet};
1112
use std::str::FromStr;
1213

14+
// Batch size for `sync_with_electrum`.
15+
const BATCH_SIZE: usize = 5;
16+
1317
fn get_balance(
1418
recv_chain: &LocalChain,
1519
recv_graph: &IndexedTxGraph<ConfirmationBlockTime, SpkTxOutIndex<()>>,
@@ -22,6 +26,39 @@ fn get_balance(
2226
Ok(balance)
2327
}
2428

29+
fn sync_with_electrum<I, Spks>(
30+
client: &BdkElectrumClient<electrum_client::Client>,
31+
spks: Spks,
32+
chain: &mut LocalChain,
33+
graph: &mut IndexedTxGraph<ConfirmationBlockTime, I>,
34+
) -> anyhow::Result<SyncResult>
35+
where
36+
I: Indexer,
37+
I::ChangeSet: Default + Merge,
38+
Spks: IntoIterator<Item = ScriptBuf>,
39+
Spks::IntoIter: ExactSizeIterator + Send + 'static,
40+
{
41+
let mut update = client.sync(
42+
SyncRequest::from_chain_tip(chain.tip()).chain_spks(spks),
43+
BATCH_SIZE,
44+
true,
45+
)?;
46+
47+
// Update `last_seen` to be able to calculate balance for unconfirmed transactions.
48+
let now = std::time::UNIX_EPOCH
49+
.elapsed()
50+
.expect("must get time")
51+
.as_secs();
52+
let _ = update.graph_update.update_last_seen_unconfirmed(now);
53+
54+
let _ = chain
55+
.apply_update(update.chain_update.clone())
56+
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
57+
let _ = graph.apply_update(update.graph_update.clone());
58+
59+
Ok(update)
60+
}
61+
2562
#[test]
2663
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
2764
let env = TestEnv::new()?;
@@ -60,7 +97,7 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
6097
None,
6198
)?;
6299
env.mine_blocks(1, None)?;
63-
env.wait_until_electrum_sees_block()?;
100+
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
64101

65102
// use a full checkpoint linked list (since this is not what we are testing)
66103
let cp_tip = env.make_checkpoint_tip();
@@ -162,7 +199,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
162199
None,
163200
)?;
164201
env.mine_blocks(1, None)?;
165-
env.wait_until_electrum_sees_block()?;
202+
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
166203

167204
// use a full checkpoint linked list (since this is not what we are testing)
168205
let cp_tip = env.make_checkpoint_tip();
@@ -204,7 +241,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
204241
None,
205242
)?;
206243
env.mine_blocks(1, None)?;
207-
env.wait_until_electrum_sees_block()?;
244+
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
208245

209246
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
210247
// The last active indice won't be updated in the first case but will in the second one.
@@ -238,14 +275,11 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
238275
Ok(())
239276
}
240277

241-
/// Ensure that [`ElectrumExt`] can sync properly.
242-
///
243-
/// 1. Mine 101 blocks.
244-
/// 2. Send a tx.
245-
/// 3. Mine extra block to confirm sent tx.
246-
/// 4. Check [`Balance`] to ensure tx is confirmed.
278+
/// Ensure that [`BdkElectrumClient::sync`] can confirm previously unconfirmed transactions in both
279+
/// reorg and no-reorg situations. After the transaction is confirmed after reorg, check if floating
280+
/// txouts for previous outputs were inserted for transaction fee calculation.
247281
#[test]
248-
fn scan_detects_confirmed_tx() -> anyhow::Result<()> {
282+
fn test_sync() -> anyhow::Result<()> {
249283
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
250284

251285
let env = TestEnv::new()?;
@@ -271,35 +305,88 @@ fn scan_detects_confirmed_tx() -> anyhow::Result<()> {
271305

272306
// Mine some blocks.
273307
env.mine_blocks(101, Some(addr_to_mine))?;
308+
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
274309

275-
// Create transaction that is tracked by our receiver.
276-
env.send(&addr_to_track, SEND_AMOUNT)?;
310+
// Broadcast transaction to mempool.
311+
let txid = env.send(&addr_to_track, SEND_AMOUNT)?;
312+
env.wait_until_electrum_sees_txid(txid, Duration::from_secs(6))?;
277313

278-
// Mine a block to confirm sent tx.
314+
sync_with_electrum(
315+
&client,
316+
[spk_to_track.clone()],
317+
&mut recv_chain,
318+
&mut recv_graph,
319+
)?;
320+
321+
// Check for unconfirmed balance when transaction exists only in mempool.
322+
assert_eq!(
323+
get_balance(&recv_chain, &recv_graph)?,
324+
Balance {
325+
trusted_pending: SEND_AMOUNT,
326+
..Balance::default()
327+
},
328+
"balance must be correct",
329+
);
330+
331+
// Mine block to confirm transaction.
279332
env.mine_blocks(1, None)?;
333+
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
280334

281-
// Sync up to tip.
282-
env.wait_until_electrum_sees_block()?;
283-
let update = client.sync(
284-
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks(core::iter::once(spk_to_track)),
285-
5,
286-
true,
335+
sync_with_electrum(
336+
&client,
337+
[spk_to_track.clone()],
338+
&mut recv_chain,
339+
&mut recv_graph,
287340
)?;
288341

289-
let _ = recv_chain
290-
.apply_update(update.chain_update)
291-
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
292-
let _ = recv_graph.apply_update(update.graph_update);
342+
// Check if balance is correct when transaction is confirmed.
343+
assert_eq!(
344+
get_balance(&recv_chain, &recv_graph)?,
345+
Balance {
346+
confirmed: SEND_AMOUNT,
347+
..Balance::default()
348+
},
349+
"balance must be correct",
350+
);
351+
352+
// Perform reorg on block with confirmed transaction.
353+
env.reorg_empty_blocks(1)?;
354+
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
355+
356+
sync_with_electrum(
357+
&client,
358+
[spk_to_track.clone()],
359+
&mut recv_chain,
360+
&mut recv_graph,
361+
)?;
293362

294-
// Check to see if tx is confirmed.
363+
// Check if balance is correct when transaction returns to mempool.
364+
assert_eq!(
365+
get_balance(&recv_chain, &recv_graph)?,
366+
Balance {
367+
trusted_pending: SEND_AMOUNT,
368+
..Balance::default()
369+
},
370+
);
371+
372+
// Mine block to confirm transaction again.
373+
env.mine_blocks(1, None)?;
374+
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
375+
376+
sync_with_electrum(&client, [spk_to_track], &mut recv_chain, &mut recv_graph)?;
377+
378+
// Check if balance is correct once transaction is confirmed again.
295379
assert_eq!(
296380
get_balance(&recv_chain, &recv_graph)?,
297381
Balance {
298382
confirmed: SEND_AMOUNT,
299383
..Balance::default()
300384
},
385+
"balance must be correct",
301386
);
302387

388+
// Check to see if we have the floating txouts available from our transactions' previous outputs
389+
// in order to calculate transaction fees.
303390
for tx in recv_graph.graph().full_txs() {
304391
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
305392
// floating txouts available from the transaction's previous outputs.
@@ -371,18 +458,14 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
371458
}
372459

373460
// Sync up to tip.
374-
env.wait_until_electrum_sees_block()?;
375-
let update = client.sync(
376-
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
377-
5,
378-
false,
461+
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
462+
let update = sync_with_electrum(
463+
&client,
464+
[spk_to_track.clone()],
465+
&mut recv_chain,
466+
&mut recv_graph,
379467
)?;
380468

381-
let _ = recv_chain
382-
.apply_update(update.chain_update)
383-
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
384-
let _ = recv_graph.apply_update(update.graph_update.clone());
385-
386469
// Retain a snapshot of all anchors before reorg process.
387470
let initial_anchors = update.graph_update.all_anchors();
388471
let anchors: Vec<_> = initial_anchors.iter().cloned().collect();
@@ -407,24 +490,21 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
407490
for depth in 1..=REORG_COUNT {
408491
env.reorg_empty_blocks(depth)?;
409492

410-
env.wait_until_electrum_sees_block()?;
411-
let update = client.sync(
412-
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
413-
5,
414-
false,
493+
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
494+
let update = sync_with_electrum(
495+
&client,
496+
[spk_to_track.clone()],
497+
&mut recv_chain,
498+
&mut recv_graph,
415499
)?;
416500

417-
let _ = recv_chain
418-
.apply_update(update.chain_update)
419-
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
420-
421501
// Check that no new anchors are added during current reorg.
422502
assert!(initial_anchors.is_superset(update.graph_update.all_anchors()));
423-
let _ = recv_graph.apply_update(update.graph_update);
424503

425504
assert_eq!(
426505
get_balance(&recv_chain, &recv_graph)?,
427506
Balance {
507+
trusted_pending: SEND_AMOUNT * depth as u64,
428508
confirmed: SEND_AMOUNT * (REORG_COUNT - depth) as u64,
429509
..Balance::default()
430510
},

crates/testenv/src/lib.rs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,22 +187,48 @@ impl TestEnv {
187187
}
188188

189189
/// This method waits for the Electrum notification indicating that a new block has been mined.
190-
pub fn wait_until_electrum_sees_block(&self) -> anyhow::Result<()> {
190+
/// `timeout` is the maximum [`Duration`] we want to wait for a response from Electrsd.
191+
pub fn wait_until_electrum_sees_block(&self, timeout: Duration) -> anyhow::Result<()> {
191192
self.electrsd.client.block_headers_subscribe()?;
192-
let mut delay = Duration::from_millis(64);
193+
let delay = Duration::from_millis(200);
194+
let start = std::time::Instant::now();
193195

194-
loop {
196+
while start.elapsed() < timeout {
195197
self.electrsd.trigger()?;
196198
self.electrsd.client.ping()?;
197199
if self.electrsd.client.block_headers_pop()?.is_some() {
198200
return Ok(());
199201
}
200202

201-
if delay.as_millis() < 512 {
202-
delay = delay.mul_f32(2.0);
203+
std::thread::sleep(delay);
204+
}
205+
206+
Err(anyhow::Error::msg(
207+
"Timed out waiting for Electrsd to get block header",
208+
))
209+
}
210+
211+
/// This method waits for Electrsd to see a transaction with given `txid`. `timeout` is the
212+
/// maximum [`Duration`] we want to wait for a response from Electrsd.
213+
pub fn wait_until_electrum_sees_txid(
214+
&self,
215+
txid: Txid,
216+
timeout: Duration,
217+
) -> anyhow::Result<()> {
218+
let delay = Duration::from_millis(200);
219+
let start = std::time::Instant::now();
220+
221+
while start.elapsed() < timeout {
222+
if self.electrsd.client.transaction_get(&txid).is_ok() {
223+
return Ok(());
203224
}
225+
204226
std::thread::sleep(delay);
205227
}
228+
229+
Err(anyhow::Error::msg(
230+
"Timed out waiting for Electrsd to get transaction",
231+
))
206232
}
207233

208234
/// Invalidate a number of blocks of a given size `count`.
@@ -285,6 +311,7 @@ impl TestEnv {
285311
#[cfg(test)]
286312
mod test {
287313
use crate::TestEnv;
314+
use core::time::Duration;
288315
use electrsd::bitcoind::{anyhow::Result, bitcoincore_rpc::RpcApi};
289316

290317
/// This checks that reorgs initiated by `bitcoind` is detected by our `electrsd` instance.
@@ -294,15 +321,15 @@ mod test {
294321

295322
// Mine some blocks.
296323
env.mine_blocks(101, None)?;
297-
env.wait_until_electrum_sees_block()?;
324+
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
298325
let height = env.bitcoind.client.get_block_count()?;
299326
let blocks = (0..=height)
300327
.map(|i| env.bitcoind.client.get_block_hash(i))
301328
.collect::<Result<Vec<_>, _>>()?;
302329

303330
// Perform reorg on six blocks.
304331
env.reorg(6)?;
305-
env.wait_until_electrum_sees_block()?;
332+
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
306333
let reorged_height = env.bitcoind.client.get_block_count()?;
307334
let reorged_blocks = (0..=height)
308335
.map(|i| env.bitcoind.client.get_block_hash(i))

0 commit comments

Comments
 (0)