Merge bitcoindevkit/bdk#652: Fix: Hang when ElectrumBlockchainConfig::stop_gap == 0
				
					
				
			612da165f8cfbc2b0aee0a37b2cdc44d6da017c8 `Blockchain` stop_gap testing improvements (志宇) 8a5f89e129d421a41af02ea85383d5b82f5ff665 Fix hang when `ElectrumBlockchainConfig::stop_gap == 0` (志宇) Pull request description: * Ensure `chunk_size` is > 0 during wallet sync. * Slight refactoring for better readability. * Add test: `test_electrum_blockchain_factory_sync_with_stop_gaps` <!-- You can erase any parts of this template not applicable to your Pull Request. --> ### Description `Wallet::sync` hangs indefinitely when syncing with Electrum with `stop_gap` set as 0. The culprit is having `chunk_size` set as `stop_gap`. A zero value results in syncing not being able to progress. Fixes #651 ### 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 #### Bugfixes: ~* [ ] This pull request breaks the existing API~ * [x] I've added tests to reproduce the issue which are now passing * [x] I'm linking the issue being fixed by this PR ACKs for top commit: afilini: ACK 612da165f8cfbc2b0aee0a37b2cdc44d6da017c8 Tree-SHA512: 56f1bff788855facc21856209922594cff9f639c5c58ecd180a0493322a75a564b72ded330ab0b6d6c90007ce859d2b8a5d2870d619bae5ddf9a3d64837f3753
This commit is contained in:
		
						commit
						0e92820af4
					
				| @ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||||||
| ## [Unreleased] | ## [Unreleased] | ||||||
| - New MSRV set to `1.56.1` | - New MSRV set to `1.56.1` | ||||||
| - Fee sniping discouraging through nLockTime - if the user specifies a `current_height`, we use that as a nlocktime, otherwise we use the last sync height (or 0 if we never synced) | - Fee sniping discouraging through nLockTime - if the user specifies a `current_height`, we use that as a nlocktime, otherwise we use the last sync height (or 0 if we never synced) | ||||||
|  | - Fix hang when `ElectrumBlockchainConfig::stop_gap` is zero. | ||||||
| 
 | 
 | ||||||
| ## [v0.19.0] - [v0.18.0] | ## [v0.19.0] - [v0.18.0] | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -108,7 +108,10 @@ impl WalletSync for ElectrumBlockchain { | |||||||
|         let mut block_times = HashMap::<u32, u32>::new(); |         let mut block_times = HashMap::<u32, u32>::new(); | ||||||
|         let mut txid_to_height = HashMap::<Txid, u32>::new(); |         let mut txid_to_height = HashMap::<Txid, u32>::new(); | ||||||
|         let mut tx_cache = TxCache::new(database, &self.client); |         let mut tx_cache = TxCache::new(database, &self.client); | ||||||
|         let chunk_size = self.stop_gap; | 
 | ||||||
|  |         // Set chunk_size to the smallest value capable of finding a gap greater than stop_gap.
 | ||||||
|  |         let chunk_size = self.stop_gap + 1; | ||||||
|  | 
 | ||||||
|         // The electrum server has been inconsistent somehow in its responses during sync. For
 |         // The electrum server has been inconsistent somehow in its responses during sync. For
 | ||||||
|         // example, we do a batch request of transactions and the response contains less
 |         // example, we do a batch request of transactions and the response contains less
 | ||||||
|         // tranascations than in the request. This should never happen but we don't want to panic.
 |         // tranascations than in the request. This should never happen but we don't want to panic.
 | ||||||
| @ -144,21 +147,12 @@ impl WalletSync for ElectrumBlockchain { | |||||||
| 
 | 
 | ||||||
|                 Request::Conftime(conftime_req) => { |                 Request::Conftime(conftime_req) => { | ||||||
|                     // collect up to chunk_size heights to fetch from electrum
 |                     // collect up to chunk_size heights to fetch from electrum
 | ||||||
|                     let needs_block_height = { |                     let needs_block_height = conftime_req | ||||||
|                         let mut needs_block_height_iter = conftime_req |                         .request() | ||||||
|                             .request() |                         .filter_map(|txid| txid_to_height.get(txid).cloned()) | ||||||
|                             .filter_map(|txid| txid_to_height.get(txid).cloned()) |                         .filter(|height| block_times.get(height).is_none()) | ||||||
|                             .filter(|height| block_times.get(height).is_none()); |                         .take(chunk_size) | ||||||
|                         let mut needs_block_height = HashSet::new(); |                         .collect::<HashSet<u32>>(); | ||||||
| 
 |  | ||||||
|                         while needs_block_height.len() < chunk_size { |  | ||||||
|                             match needs_block_height_iter.next() { |  | ||||||
|                                 Some(height) => needs_block_height.insert(height), |  | ||||||
|                                 None => break, |  | ||||||
|                             }; |  | ||||||
|                         } |  | ||||||
|                         needs_block_height |  | ||||||
|                     }; |  | ||||||
| 
 | 
 | ||||||
|                     let new_block_headers = self |                     let new_block_headers = self | ||||||
|                         .client |                         .client | ||||||
| @ -328,6 +322,7 @@ mod test { | |||||||
|     use super::*; |     use super::*; | ||||||
|     use crate::database::MemoryDatabase; |     use crate::database::MemoryDatabase; | ||||||
|     use crate::testutils::blockchain_tests::TestClient; |     use crate::testutils::blockchain_tests::TestClient; | ||||||
|  |     use crate::testutils::configurable_blockchain_tests::ConfigurableBlockchainTester; | ||||||
|     use crate::wallet::{AddressIndex, Wallet}; |     use crate::wallet::{AddressIndex, Wallet}; | ||||||
| 
 | 
 | ||||||
|     crate::bdk_blockchain_tests! { |     crate::bdk_blockchain_tests! { | ||||||
| @ -385,4 +380,29 @@ mod test { | |||||||
| 
 | 
 | ||||||
|         assert_eq!(wallet.get_balance().unwrap(), 50_000); |         assert_eq!(wallet.get_balance().unwrap(), 50_000); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_electrum_with_variable_configs() { | ||||||
|  |         struct ElectrumTester; | ||||||
|  | 
 | ||||||
|  |         impl ConfigurableBlockchainTester<ElectrumBlockchain> for ElectrumTester { | ||||||
|  |             const BLOCKCHAIN_NAME: &'static str = "Electrum"; | ||||||
|  | 
 | ||||||
|  |             fn config_with_stop_gap( | ||||||
|  |                 &self, | ||||||
|  |                 test_client: &mut TestClient, | ||||||
|  |                 stop_gap: usize, | ||||||
|  |             ) -> Option<ElectrumBlockchainConfig> { | ||||||
|  |                 Some(ElectrumBlockchainConfig { | ||||||
|  |                     url: test_client.electrsd.electrum_url.clone(), | ||||||
|  |                     socks5: None, | ||||||
|  |                     retry: 0, | ||||||
|  |                     timeout: None, | ||||||
|  |                     stop_gap: stop_gap, | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         ElectrumTester.run(); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -209,4 +209,38 @@ mod test { | |||||||
|             "should inherit from value for 25" |             "should inherit from value for 25" | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     #[cfg(feature = "test-esplora")] | ||||||
|  |     fn test_esplora_with_variable_configs() { | ||||||
|  |         use crate::testutils::{ | ||||||
|  |             blockchain_tests::TestClient, | ||||||
|  |             configurable_blockchain_tests::ConfigurableBlockchainTester, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         struct EsploraTester; | ||||||
|  | 
 | ||||||
|  |         impl ConfigurableBlockchainTester<EsploraBlockchain> for EsploraTester { | ||||||
|  |             const BLOCKCHAIN_NAME: &'static str = "Esplora"; | ||||||
|  | 
 | ||||||
|  |             fn config_with_stop_gap( | ||||||
|  |                 &self, | ||||||
|  |                 test_client: &mut TestClient, | ||||||
|  |                 stop_gap: usize, | ||||||
|  |             ) -> Option<EsploraBlockchainConfig> { | ||||||
|  |                 Some(EsploraBlockchainConfig { | ||||||
|  |                     base_url: format!( | ||||||
|  |                         "http://{}", | ||||||
|  |                         test_client.electrsd.esplora_url.as_ref().unwrap() | ||||||
|  |                     ), | ||||||
|  |                     proxy: None, | ||||||
|  |                     concurrency: None, | ||||||
|  |                     stop_gap: stop_gap, | ||||||
|  |                     timeout: None, | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         EsploraTester.run(); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -88,7 +88,7 @@ impl Blockchain for EsploraBlockchain { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn broadcast(&self, tx: &Transaction) -> Result<(), Error> { |     fn broadcast(&self, tx: &Transaction) -> Result<(), Error> { | ||||||
|         let _txid = self.url_client._broadcast(tx)?; |         self.url_client._broadcast(tx)?; | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										140
									
								
								src/testutils/configurable_blockchain_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/testutils/configurable_blockchain_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,140 @@ | |||||||
|  | use bitcoin::Network; | ||||||
|  | 
 | ||||||
|  | use crate::{ | ||||||
|  |     blockchain::ConfigurableBlockchain, database::MemoryDatabase, testutils, wallet::AddressIndex, | ||||||
|  |     Wallet, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use super::blockchain_tests::TestClient; | ||||||
|  | 
 | ||||||
|  | /// Trait for testing [`ConfigurableBlockchain`] implementations.
 | ||||||
|  | pub trait ConfigurableBlockchainTester<B: ConfigurableBlockchain>: Sized { | ||||||
|  |     /// Blockchain name for logging.
 | ||||||
|  |     const BLOCKCHAIN_NAME: &'static str; | ||||||
|  | 
 | ||||||
|  |     /// Generates a blockchain config with a given stop_gap.
 | ||||||
|  |     ///
 | ||||||
|  |     /// If this returns [`Option::None`], then the associated tests will not run.
 | ||||||
|  |     fn config_with_stop_gap( | ||||||
|  |         &self, | ||||||
|  |         _test_client: &mut TestClient, | ||||||
|  |         _stop_gap: usize, | ||||||
|  |     ) -> Option<B::Config> { | ||||||
|  |         None | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Runs all avaliable tests.
 | ||||||
|  |     fn run(&self) { | ||||||
|  |         let test_client = &mut TestClient::default(); | ||||||
|  | 
 | ||||||
|  |         if self.config_with_stop_gap(test_client, 0).is_some() { | ||||||
|  |             test_wallet_sync_with_stop_gaps(test_client, self); | ||||||
|  |         } else { | ||||||
|  |             println!( | ||||||
|  |                 "{}: Skipped tests requiring config_with_stop_gap.", | ||||||
|  |                 Self::BLOCKCHAIN_NAME | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Test whether blockchain implementation syncs with expected behaviour given different `stop_gap`
 | ||||||
|  | /// parameters.
 | ||||||
|  | ///
 | ||||||
|  | /// For each test vector:
 | ||||||
|  | /// * Fill wallet's derived addresses with balances (as specified by test vector).
 | ||||||
|  | ///    * [0..addrs_before]          => 1000sats for each address
 | ||||||
|  | ///    * [addrs_before..actual_gap] => empty addresses
 | ||||||
|  | ///    * [actual_gap..addrs_after]  => 1000sats for each address
 | ||||||
|  | /// * Then, perform wallet sync and obtain wallet balance
 | ||||||
|  | /// * Check balance is within expected range (we can compare `stop_gap` and `actual_gap` to
 | ||||||
|  | ///    determine this).
 | ||||||
|  | fn test_wallet_sync_with_stop_gaps<T, B>(test_client: &mut TestClient, tester: &T) | ||||||
|  | where | ||||||
|  |     T: ConfigurableBlockchainTester<B>, | ||||||
|  |     B: ConfigurableBlockchain, | ||||||
|  | { | ||||||
|  |     // Generates wallet descriptor
 | ||||||
|  |     let descriptor_of_account = |account_index: usize| -> String { | ||||||
|  |         format!("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/{account_index}/*)") | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // Amount (in satoshis) provided to a single address (which expects to have a balance)
 | ||||||
|  |     const AMOUNT_PER_TX: u64 = 1000; | ||||||
|  | 
 | ||||||
|  |     // [stop_gap, actual_gap, addrs_before, addrs_after]
 | ||||||
|  |     //
 | ||||||
|  |     // [0]     stop_gap: Passed to [`ElectrumBlockchainConfig`]
 | ||||||
|  |     // [1]   actual_gap: Range size of address indexes without a balance
 | ||||||
|  |     // [2] addrs_before: Range size of address indexes (before gap) which contains a balance
 | ||||||
|  |     // [3]  addrs_after: Range size of address indexes (after gap) which contains a balance
 | ||||||
|  |     let test_vectors: Vec<[u64; 4]> = vec![ | ||||||
|  |         [0, 0, 0, 5], | ||||||
|  |         [0, 0, 5, 5], | ||||||
|  |         [0, 1, 5, 5], | ||||||
|  |         [0, 2, 5, 5], | ||||||
|  |         [1, 0, 5, 5], | ||||||
|  |         [1, 1, 5, 5], | ||||||
|  |         [1, 2, 5, 5], | ||||||
|  |         [2, 1, 5, 5], | ||||||
|  |         [2, 2, 5, 5], | ||||||
|  |         [2, 3, 5, 5], | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     for (account_index, vector) in test_vectors.into_iter().enumerate() { | ||||||
|  |         let [stop_gap, actual_gap, addrs_before, addrs_after] = vector; | ||||||
|  |         let descriptor = descriptor_of_account(account_index); | ||||||
|  | 
 | ||||||
|  |         let blockchain = B::from_config( | ||||||
|  |             &tester | ||||||
|  |                 .config_with_stop_gap(test_client, stop_gap as _) | ||||||
|  |                 .unwrap(), | ||||||
|  |         ) | ||||||
|  |         .unwrap(); | ||||||
|  | 
 | ||||||
|  |         let wallet = | ||||||
|  |             Wallet::new(&descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap(); | ||||||
|  | 
 | ||||||
|  |         // fill server-side with txs to specified address indexes
 | ||||||
|  |         // return the max balance of the wallet (also the actual balance)
 | ||||||
|  |         let max_balance = (0..addrs_before) | ||||||
|  |             .chain(addrs_before + actual_gap..addrs_before + actual_gap + addrs_after) | ||||||
|  |             .fold(0_u64, |sum, i| { | ||||||
|  |                 let address = wallet.get_address(AddressIndex::Peek(i as _)).unwrap(); | ||||||
|  |                 test_client.receive(testutils! { | ||||||
|  |                     @tx ( (@addr address.address) => AMOUNT_PER_TX ) | ||||||
|  |                 }); | ||||||
|  |                 sum + AMOUNT_PER_TX | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |         // minimum allowed balance of wallet (based on stop gap)
 | ||||||
|  |         let min_balance = if actual_gap > stop_gap { | ||||||
|  |             addrs_before * AMOUNT_PER_TX | ||||||
|  |         } else { | ||||||
|  |             max_balance | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         // perform wallet sync
 | ||||||
|  |         wallet.sync(&blockchain, Default::default()).unwrap(); | ||||||
|  | 
 | ||||||
|  |         let wallet_balance = wallet.get_balance().unwrap(); | ||||||
|  | 
 | ||||||
|  |         let details = format!( | ||||||
|  |             "test_vector: [stop_gap: {}, actual_gap: {}, addrs_before: {}, addrs_after: {}]", | ||||||
|  |             stop_gap, actual_gap, addrs_before, addrs_after, | ||||||
|  |         ); | ||||||
|  |         assert!( | ||||||
|  |             wallet_balance <= max_balance, | ||||||
|  |             "wallet balance is greater than received amount: {}", | ||||||
|  |             details | ||||||
|  |         ); | ||||||
|  |         assert!( | ||||||
|  |             wallet_balance >= min_balance, | ||||||
|  |             "wallet balance is smaller than expected: {}", | ||||||
|  |             details | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         // generate block to confirm new transactions
 | ||||||
|  |         test_client.generate(1, None); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -14,6 +14,10 @@ | |||||||
| #[cfg(feature = "test-blockchains")] | #[cfg(feature = "test-blockchains")] | ||||||
| pub mod blockchain_tests; | pub mod blockchain_tests; | ||||||
| 
 | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | #[cfg(feature = "test-blockchains")] | ||||||
|  | pub mod configurable_blockchain_tests; | ||||||
|  | 
 | ||||||
| use bitcoin::{Address, Txid}; | use bitcoin::{Address, Txid}; | ||||||
| 
 | 
 | ||||||
| #[derive(Clone, Debug)] | #[derive(Clone, Debug)] | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user