Fix hang when ElectrumBlockchainConfig::stop_gap == 0
				
					
				
			* Ensure chunk_size is > 0 during wallet sync. * Slight refactoring for better readability. * Add test: test_electrum_blockchain_factory_sync_with_stop_gaps
This commit is contained in:
		
							parent
							
								
									063d51fd75
								
							
						
					
					
						commit
						8a5f89e129
					
				| @ -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.
 | ||||||
| @ -145,18 +148,16 @@ 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 = { | ||||||
|                         let mut needs_block_height_iter = conftime_req |                         let mut needs_block_height = HashSet::with_capacity(chunk_size); | ||||||
|  |                         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()) | ||||||
|                         let mut needs_block_height = HashSet::new(); |                             .take(chunk_size) | ||||||
|  |                             .for_each(|height| { | ||||||
|  |                                 needs_block_height.insert(height); | ||||||
|  |                             }); | ||||||
| 
 | 
 | ||||||
|                         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 |                         needs_block_height | ||||||
|                     }; |                     }; | ||||||
| 
 | 
 | ||||||
| @ -385,4 +386,116 @@ mod test { | |||||||
| 
 | 
 | ||||||
|         assert_eq!(wallet.get_balance().unwrap(), 50_000); |         assert_eq!(wallet.get_balance().unwrap(), 50_000); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_electrum_blockchain_factory_sync_with_stop_gaps() { | ||||||
|  |         // Test whether Electrum blockchain 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).
 | ||||||
|  | 
 | ||||||
|  |         // 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], | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         let mut test_client = TestClient::default(); | ||||||
|  | 
 | ||||||
|  |         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 factory = Arc::new( | ||||||
|  |                 ElectrumBlockchain::from_config(&ElectrumBlockchainConfig { | ||||||
|  |                     url: test_client.electrsd.electrum_url.clone(), | ||||||
|  |                     socks5: None, | ||||||
|  |                     retry: 0, | ||||||
|  |                     timeout: None, | ||||||
|  |                     stop_gap: stop_gap as _, | ||||||
|  |                 }) | ||||||
|  |                 .unwrap(), | ||||||
|  |             ); | ||||||
|  |             let wallet = Wallet::new( | ||||||
|  |                 descriptor.as_str(), | ||||||
|  |                 None, | ||||||
|  |                 bitcoin::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(); | ||||||
|  |                     let tx = testutils! { | ||||||
|  |                         @tx ( (@addr address.address) => AMOUNT_PER_TX ) | ||||||
|  |                     }; | ||||||
|  |                     test_client.receive(tx); | ||||||
|  |                     sum + AMOUNT_PER_TX | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |             // generate blocks to confirm new transactions
 | ||||||
|  |             test_client.generate(3, None); | ||||||
|  | 
 | ||||||
|  |             // 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
 | ||||||
|  |             factory | ||||||
|  |                 .sync_wallet(&wallet, None, 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 | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user