]> Untitled Git - bdk/commitdiff
fix: define and document `stop_gap`
authorJose Storopoli <jose@storopoli.io>
Tue, 26 Mar 2024 15:44:03 +0000 (12:44 -0300)
committerJose Storopoli <jose@storopoli.io>
Tue, 26 Mar 2024 15:44:03 +0000 (12:44 -0300)
- changes the code implementation to "the maximum number of consecutive unused addresses"
  in esplora async and blocking extjensions.
- treat `stop_gap = 0` as `stop_gap = 1` for all purposes.
- renames `past_gap_limit` to `gap_limit_reached` to indicate we want to break once the gap
  limit is reached and not go further in `full_scan`, as suggested in
  https://github.com/bitcoindevkit/bdk/issues/1227#issuecomment-1859040463
- change the tests according to the new implementation.
- add notes on what `stop_gap` means and links to convergent definition in other
  Bitcoin-related software.

Closes #1227

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

index 4d6e0dfa834e1e7c5e80e60e7960c323eed7685f..9e25eedfbfec19cf584089fa9029b03f0a8eddc2 100644 (file)
@@ -52,6 +52,19 @@ pub trait EsploraAsyncExt {
     /// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
     /// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
     /// parallel.
+    ///
+    /// ## Note
+    ///
+    /// `stop_gap` is defined as "the maximum number of consecutive unused addresses".
+    /// For example, with a `stop_gap` of  3, `full_scan` will keep scanning
+    /// until it encounters 3 consecutive script pubkeys with no associated transactions.
+    ///
+    /// This follows the same approach as other Bitcoin-related software,
+    /// such as [Electrum](https://electrum.readthedocs.io/en/latest/faq.html#what-is-the-gap-limit),
+    /// [BTCPay Server](https://docs.btcpayserver.org/FAQ/Wallet/#the-gap-limit-problem),
+    /// and [Sparrow](https://www.sparrowwallet.com/docs/faq.html#ive-restored-my-wallet-but-some-of-my-funds-are-missing).
+    ///
+    /// A `stop_gap` of 0 will be treated as a `stop_gap` of 1.
     async fn full_scan<K: Ord + Clone + Send>(
         &self,
         keychain_spks: BTreeMap<
@@ -162,6 +175,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
         let parallel_requests = Ord::max(parallel_requests, 1);
         let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
         let mut last_active_indexes = BTreeMap::<K, u32>::new();
+        let stop_gap = Ord::max(stop_gap, 1);
 
         for (keychain, spks) in keychain_spks {
             let mut spks = spks.into_iter();
@@ -226,12 +240,12 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
                 }
 
                 let last_index = last_index.expect("Must be set since handles wasn't empty.");
-                let past_gap_limit = if let Some(i) = last_active_index {
-                    last_index > i.saturating_add(stop_gap as u32)
+                let gap_limit_reached = if let Some(i) = last_active_index {
+                    last_index >= i.saturating_add(stop_gap as u32)
                 } else {
-                    last_index >= stop_gap as u32
+                    last_index + 1 >= stop_gap as u32
                 };
-                if past_gap_limit {
+                if gap_limit_reached {
                     break;
                 }
             }
index 993e33ac0b7931c8f98e629993f585b5a2def28b..9cd11e81916cc90a8c0d982d667f26047b15c7a7 100644 (file)
@@ -50,6 +50,19 @@ pub trait EsploraExt {
     /// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
     /// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
     /// parallel.
+    ///
+    /// ## Note
+    ///
+    /// `stop_gap` is defined as "the maximum number of consecutive unused addresses".
+    /// For example, with a `stop_gap` of  3, `full_scan` will keep scanning
+    /// until it encounters 3 consecutive script pubkeys with no associated transactions.
+    ///
+    /// This follows the same approach as other Bitcoin-related software,
+    /// such as [Electrum](https://electrum.readthedocs.io/en/latest/faq.html#what-is-the-gap-limit),
+    /// [BTCPay Server](https://docs.btcpayserver.org/FAQ/Wallet/#the-gap-limit-problem),
+    /// and [Sparrow](https://www.sparrowwallet.com/docs/faq.html#ive-restored-my-wallet-but-some-of-my-funds-are-missing).
+    ///
+    /// A `stop_gap` of 0 will be treated as a `stop_gap` of 1.
     fn full_scan<K: Ord + Clone>(
         &self,
         keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
@@ -149,6 +162,7 @@ impl EsploraExt for esplora_client::BlockingClient {
         let parallel_requests = Ord::max(parallel_requests, 1);
         let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
         let mut last_active_indexes = BTreeMap::<K, u32>::new();
+        let stop_gap = Ord::max(stop_gap, 1);
 
         for (keychain, spks) in keychain_spks {
             let mut spks = spks.into_iter();
@@ -216,12 +230,12 @@ impl EsploraExt for esplora_client::BlockingClient {
                 }
 
                 let last_index = last_index.expect("Must be set since handles wasn't empty.");
-                let past_gap_limit = if let Some(i) = last_active_index {
-                    last_index > i.saturating_add(stop_gap as u32)
+                let gap_limit_reached = if let Some(i) = last_active_index {
+                    last_index >= i.saturating_add(stop_gap as u32)
                 } else {
-                    last_index >= stop_gap as u32
+                    last_index + 1 >= stop_gap as u32
                 };
-                if past_gap_limit {
+                if gap_limit_reached {
                     break;
                 }
             }
index 6c3c9cf1f993dde6b90f206cb7b0bbc7cd890042..e08d98a3c78c4d5bc20c23dafb40249068daacd1 100644 (file)
@@ -91,9 +91,9 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
     Ok(())
 }
 
-/// Test the bounds of the address scan depending on the gap limit.
+/// Test the bounds of the address scan depending on the `stop_gap`.
 #[tokio::test]
-pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
+pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
     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()?;
@@ -140,12 +140,12 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
         sleep(Duration::from_millis(10))
     }
 
-    // A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
+    // A scan with a gap limit of 3 won't find the transaction, but a scan with a gap limit of 4
     // will.
-    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 2, 1).await?;
+    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1).await?;
     assert!(graph_update.full_txs().next().is_none());
     assert!(active_indices.is_empty());
-    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1).await?;
+    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 1).await?;
     assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
     assert_eq!(active_indices[&0], 3);
 
@@ -165,14 +165,14 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
         sleep(Duration::from_millis(10))
     }
 
-    // A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
+    // A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
     // The last active indice won't be updated in the first case but will in the second one.
-    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 1).await?;
+    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 5, 1).await?;
     let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
     assert_eq!(txs.len(), 1);
     assert!(txs.contains(&txid_4th_addr));
     assert_eq!(active_indices[&0], 3);
-    let (graph_update, active_indices) = client.full_scan(keychains, 5, 1).await?;
+    let (graph_update, active_indices) = client.full_scan(keychains, 6, 1).await?;
     let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
     assert_eq!(txs.len(), 2);
     assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
index 6225a6a6ba73d56dcc51da2491b98f864c2d6258..d3795ed36aa725287fa518fabf807a2c25d7c984 100644 (file)
@@ -106,9 +106,9 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
     Ok(())
 }
 
-/// Test the bounds of the address scan depending on the gap limit.
+/// Test the bounds of the address scan depending on the `stop_gap`.
 #[test]
-pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
+pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
     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()?;
@@ -155,12 +155,12 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
         sleep(Duration::from_millis(10))
     }
 
-    // A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
+    // A scan with a stop_gap of 3 won't find the transaction, but a scan with a gap limit of 4
     // will.
-    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 2, 1)?;
+    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1)?;
     assert!(graph_update.full_txs().next().is_none());
     assert!(active_indices.is_empty());
-    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1)?;
+    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 1)?;
     assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
     assert_eq!(active_indices[&0], 3);
 
@@ -180,14 +180,14 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
         sleep(Duration::from_millis(10))
     }
 
-    // A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
+    // A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
     // The last active indice won't be updated in the first case but will in the second one.
-    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 1)?;
+    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 5, 1)?;
     let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
     assert_eq!(txs.len(), 1);
     assert!(txs.contains(&txid_4th_addr));
     assert_eq!(active_indices[&0], 3);
-    let (graph_update, active_indices) = client.full_scan(keychains, 5, 1)?;
+    let (graph_update, active_indices) = client.full_scan(keychains, 6, 1)?;
     let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
     assert_eq!(txs.len(), 2);
     assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));