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]
 | 
			
		||||
- 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)
 | 
			
		||||
- Fix hang when `ElectrumBlockchainConfig::stop_gap` is zero.
 | 
			
		||||
 | 
			
		||||
## [v0.19.0] - [v0.18.0]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -108,7 +108,10 @@ impl WalletSync for ElectrumBlockchain {
 | 
			
		||||
        let mut block_times = HashMap::<u32, u32>::new();
 | 
			
		||||
        let mut txid_to_height = HashMap::<Txid, u32>::new();
 | 
			
		||||
        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
 | 
			
		||||
        // 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.
 | 
			
		||||
@ -145,18 +148,16 @@ impl WalletSync for ElectrumBlockchain {
 | 
			
		||||
                Request::Conftime(conftime_req) => {
 | 
			
		||||
                    // collect up to chunk_size heights to fetch from electrum
 | 
			
		||||
                    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()
 | 
			
		||||
                            .filter_map(|txid| txid_to_height.get(txid).cloned())
 | 
			
		||||
                            .filter(|height| block_times.get(height).is_none());
 | 
			
		||||
                        let mut needs_block_height = HashSet::new();
 | 
			
		||||
                            .filter(|height| block_times.get(height).is_none())
 | 
			
		||||
                            .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
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
@ -385,4 +386,116 @@ mod test {
 | 
			
		||||
 | 
			
		||||
        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