Skip to content

Commit 55b680c

Browse files
committed
Merge #1225: esplora: fix incorrect gap limit check in blocking client
43aed38 esplora: also test the gap limit bounds in the async client (Antoine Poinsot) cb713e5 esplora: also fix the gap limit check in the async client (Antoine Poinsot) 2c4e90a esplora: scan gap limit bounds testing (Antoine Poinsot) 18bd329 esplora: fix incorrect gap limit check in blocking client (Antoine Poinsot) Pull request description: The gap limit was checked such as if the last_index was not None but the last_active_index was, we'd consider having reached it. But the last_index is never None for this check. This effectively made it so the gap limit was always 1: if the first address isn't used last_active_index will be None and we'd return immediately. Fix this by avoiding error-prone Option comparisons and correctly checking we've reached the gap limit before breaking out of the loop. ACKs for top commit: danielabrozzoni: ACK 43aed38 evanlinjin: ACK 43aed38 Tree-SHA512: c6a172e0627380add56aec79e7fe36c0358e092b59f0bea8885d3524e65c10336291943efe6aeb5cc83f90c4f2146ed63215e56e08583d703b6ab57284487f03
2 parents 9e681b3 + 43aed38 commit 55b680c

File tree

4 files changed

+247
-2
lines changed

4 files changed

+247
-2
lines changed

crates/esplora/src/async_ext.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,13 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
259259
}
260260
}
261261

262-
if last_index > last_active_index.map(|i| i.saturating_add(stop_gap as u32)) {
262+
let last_index = last_index.expect("Must be set since handles wasn't empty.");
263+
let past_gap_limit = if let Some(i) = last_active_index {
264+
last_index > i.saturating_add(stop_gap as u32)
265+
} else {
266+
last_index >= stop_gap as u32
267+
};
268+
if past_gap_limit {
263269
break;
264270
}
265271
}

crates/esplora/src/blocking_ext.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,13 @@ impl EsploraExt for esplora_client::BlockingClient {
250250
}
251251
}
252252

253-
if last_index > last_active_index.map(|i| i.saturating_add(stop_gap as u32)) {
253+
let last_index = last_index.expect("Must be set since handles wasn't empty.");
254+
let past_gap_limit = if let Some(i) = last_active_index {
255+
last_index > i.saturating_add(stop_gap as u32)
256+
} else {
257+
last_index >= stop_gap as u32
258+
};
259+
if past_gap_limit {
254260
break;
255261
}
256262
}

crates/esplora/tests/async_ext.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
33
use electrsd::bitcoind::{self, anyhow, BitcoinD};
44
use electrsd::{Conf, ElectrsD};
55
use esplora_client::{self, AsyncClient, Builder};
6+
use std::collections::{BTreeMap, HashSet};
67
use std::str::FromStr;
78
use std::thread::sleep;
89
use std::time::Duration;
@@ -115,3 +116,121 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
115116
assert_eq!(graph_update_txids, expected_txids);
116117
Ok(())
117118
}
119+
120+
/// Test the bounds of the address scan depending on the gap limit.
121+
#[tokio::test]
122+
pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
123+
let env = TestEnv::new()?;
124+
let _block_hashes = env.mine_blocks(101, None)?;
125+
126+
// Now let's test the gap limit. First of all get a chain of 10 addresses.
127+
let addresses = [
128+
"bcrt1qj9f7r8r3p2y0sqf4r3r62qysmkuh0fzep473d2ar7rcz64wqvhssjgf0z4",
129+
"bcrt1qmm5t0ch7vh2hryx9ctq3mswexcugqe4atkpkl2tetm8merqkthas3w7q30",
130+
"bcrt1qut9p7ej7l7lhyvekj28xknn8gnugtym4d5qvnp5shrsr4nksmfqsmyn87g",
131+
"bcrt1qqz0xtn3m235p2k96f5wa2dqukg6shxn9n3txe8arlrhjh5p744hsd957ww",
132+
"bcrt1q9c0t62a8l6wfytmf2t9lfj35avadk3mm8g4p3l84tp6rl66m48sqrme7wu",
133+
"bcrt1qkmh8yrk2v47cklt8dytk8f3ammcwa4q7dzattedzfhqzvfwwgyzsg59zrh",
134+
"bcrt1qvgrsrzy07gjkkfr5luplt0azxtfwmwq5t62gum5jr7zwcvep2acs8hhnp2",
135+
"bcrt1qw57edarcg50ansq8mk3guyrk78rk0fwvrds5xvqeupteu848zayq549av8",
136+
"bcrt1qvtve5ekf6e5kzs68knvnt2phfw6a0yjqrlgat392m6zt9jsvyxhqfx67ef",
137+
"bcrt1qw03ddumfs9z0kcu76ln7jrjfdwam20qtffmkcral3qtza90sp9kqm787uk",
138+
];
139+
let addresses: Vec<_> = addresses
140+
.into_iter()
141+
.map(|s| Address::from_str(s).unwrap().assume_checked())
142+
.collect();
143+
let spks: Vec<_> = addresses
144+
.iter()
145+
.enumerate()
146+
.map(|(i, addr)| (i as u32, addr.script_pubkey()))
147+
.collect();
148+
let mut keychains = BTreeMap::new();
149+
keychains.insert(0, spks);
150+
151+
// Then receive coins on the 4th address.
152+
let txid_4th_addr = env.bitcoind.client.send_to_address(
153+
&addresses[3],
154+
Amount::from_sat(10000),
155+
None,
156+
None,
157+
None,
158+
None,
159+
Some(1),
160+
None,
161+
)?;
162+
let _block_hashes = env.mine_blocks(1, None)?;
163+
while env.client.get_height().await.unwrap() < 103 {
164+
sleep(Duration::from_millis(10))
165+
}
166+
167+
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
168+
// will.
169+
let (graph_update, active_indices) = env
170+
.client
171+
.scan_txs_with_keychains(
172+
keychains.clone(),
173+
vec![].into_iter(),
174+
vec![].into_iter(),
175+
2,
176+
1,
177+
)
178+
.await?;
179+
assert!(graph_update.full_txs().next().is_none());
180+
assert!(active_indices.is_empty());
181+
let (graph_update, active_indices) = env
182+
.client
183+
.scan_txs_with_keychains(
184+
keychains.clone(),
185+
vec![].into_iter(),
186+
vec![].into_iter(),
187+
3,
188+
1,
189+
)
190+
.await?;
191+
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
192+
assert_eq!(active_indices[&0], 3);
193+
194+
// Now receive a coin on the last address.
195+
let txid_last_addr = env.bitcoind.client.send_to_address(
196+
&addresses[addresses.len() - 1],
197+
Amount::from_sat(10000),
198+
None,
199+
None,
200+
None,
201+
None,
202+
Some(1),
203+
None,
204+
)?;
205+
let _block_hashes = env.mine_blocks(1, None)?;
206+
while env.client.get_height().await.unwrap() < 104 {
207+
sleep(Duration::from_millis(10))
208+
}
209+
210+
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
211+
// The last active indice won't be updated in the first case but will in the second one.
212+
let (graph_update, active_indices) = env
213+
.client
214+
.scan_txs_with_keychains(
215+
keychains.clone(),
216+
vec![].into_iter(),
217+
vec![].into_iter(),
218+
4,
219+
1,
220+
)
221+
.await?;
222+
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
223+
assert_eq!(txs.len(), 1);
224+
assert!(txs.contains(&txid_4th_addr));
225+
assert_eq!(active_indices[&0], 3);
226+
let (graph_update, active_indices) = env
227+
.client
228+
.scan_txs_with_keychains(keychains, vec![].into_iter(), vec![].into_iter(), 5, 1)
229+
.await?;
230+
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
231+
assert_eq!(txs.len(), 2);
232+
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
233+
assert_eq!(active_indices[&0], 9);
234+
235+
Ok(())
236+
}

crates/esplora/tests/blocking_ext.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
33
use electrsd::bitcoind::{self, anyhow, BitcoinD};
44
use electrsd::{Conf, ElectrsD};
55
use esplora_client::{self, BlockingClient, Builder};
6+
use std::collections::{BTreeMap, HashSet};
67
use std::str::FromStr;
78
use std::thread::sleep;
89
use std::time::Duration;
@@ -110,5 +111,118 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
110111
let mut expected_txids = vec![txid1, txid2];
111112
expected_txids.sort();
112113
assert_eq!(graph_update_txids, expected_txids);
114+
115+
Ok(())
116+
}
117+
118+
/// Test the bounds of the address scan depending on the gap limit.
119+
#[test]
120+
pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
121+
let env = TestEnv::new()?;
122+
let _block_hashes = env.mine_blocks(101, None)?;
123+
124+
// Now let's test the gap limit. First of all get a chain of 10 addresses.
125+
let addresses = [
126+
"bcrt1qj9f7r8r3p2y0sqf4r3r62qysmkuh0fzep473d2ar7rcz64wqvhssjgf0z4",
127+
"bcrt1qmm5t0ch7vh2hryx9ctq3mswexcugqe4atkpkl2tetm8merqkthas3w7q30",
128+
"bcrt1qut9p7ej7l7lhyvekj28xknn8gnugtym4d5qvnp5shrsr4nksmfqsmyn87g",
129+
"bcrt1qqz0xtn3m235p2k96f5wa2dqukg6shxn9n3txe8arlrhjh5p744hsd957ww",
130+
"bcrt1q9c0t62a8l6wfytmf2t9lfj35avadk3mm8g4p3l84tp6rl66m48sqrme7wu",
131+
"bcrt1qkmh8yrk2v47cklt8dytk8f3ammcwa4q7dzattedzfhqzvfwwgyzsg59zrh",
132+
"bcrt1qvgrsrzy07gjkkfr5luplt0azxtfwmwq5t62gum5jr7zwcvep2acs8hhnp2",
133+
"bcrt1qw57edarcg50ansq8mk3guyrk78rk0fwvrds5xvqeupteu848zayq549av8",
134+
"bcrt1qvtve5ekf6e5kzs68knvnt2phfw6a0yjqrlgat392m6zt9jsvyxhqfx67ef",
135+
"bcrt1qw03ddumfs9z0kcu76ln7jrjfdwam20qtffmkcral3qtza90sp9kqm787uk",
136+
];
137+
let addresses: Vec<_> = addresses
138+
.into_iter()
139+
.map(|s| Address::from_str(s).unwrap().assume_checked())
140+
.collect();
141+
let spks: Vec<_> = addresses
142+
.iter()
143+
.enumerate()
144+
.map(|(i, addr)| (i as u32, addr.script_pubkey()))
145+
.collect();
146+
let mut keychains = BTreeMap::new();
147+
keychains.insert(0, spks);
148+
149+
// Then receive coins on the 4th address.
150+
let txid_4th_addr = env.bitcoind.client.send_to_address(
151+
&addresses[3],
152+
Amount::from_sat(10000),
153+
None,
154+
None,
155+
None,
156+
None,
157+
Some(1),
158+
None,
159+
)?;
160+
let _block_hashes = env.mine_blocks(1, None)?;
161+
while env.client.get_height().unwrap() < 103 {
162+
sleep(Duration::from_millis(10))
163+
}
164+
165+
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
166+
// will.
167+
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
168+
keychains.clone(),
169+
vec![].into_iter(),
170+
vec![].into_iter(),
171+
2,
172+
1,
173+
)?;
174+
assert!(graph_update.full_txs().next().is_none());
175+
assert!(active_indices.is_empty());
176+
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
177+
keychains.clone(),
178+
vec![].into_iter(),
179+
vec![].into_iter(),
180+
3,
181+
1,
182+
)?;
183+
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
184+
assert_eq!(active_indices[&0], 3);
185+
186+
// Now receive a coin on the last address.
187+
let txid_last_addr = env.bitcoind.client.send_to_address(
188+
&addresses[addresses.len() - 1],
189+
Amount::from_sat(10000),
190+
None,
191+
None,
192+
None,
193+
None,
194+
Some(1),
195+
None,
196+
)?;
197+
let _block_hashes = env.mine_blocks(1, None)?;
198+
while env.client.get_height().unwrap() < 104 {
199+
sleep(Duration::from_millis(10))
200+
}
201+
202+
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
203+
// The last active indice won't be updated in the first case but will in the second one.
204+
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
205+
keychains.clone(),
206+
vec![].into_iter(),
207+
vec![].into_iter(),
208+
4,
209+
1,
210+
)?;
211+
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
212+
assert_eq!(txs.len(), 1);
213+
assert!(txs.contains(&txid_4th_addr));
214+
assert_eq!(active_indices[&0], 3);
215+
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
216+
keychains,
217+
vec![].into_iter(),
218+
vec![].into_iter(),
219+
5,
220+
1,
221+
)?;
222+
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
223+
assert_eq!(txs.len(), 2);
224+
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
225+
assert_eq!(active_indices[&0], 9);
226+
113227
Ok(())
114228
}

0 commit comments

Comments
 (0)