Fix wallet sync not finding coins of addresses which are not cached
Previously, electrum-based blockchain implementations only synced for `scriptPubKey`s that are already cached in `Database`. This PR introduces a feedback mechanism, that uses `stop_gap` and the difference between "current index" and "last active index" to determine whether we need to cache more `scriptPubKeys`. The `WalletSync::wallet_setup` trait now may return an `Error::MissingCachedScripts` error which contains the number of extra `scriptPubKey`s to cache, in order to satisfy `stop_gap` for the next call. `Wallet::sync` now calls `WalletSync` in a loop, cacheing inbetween subsequent calls (if needed).
This commit is contained in:
@@ -743,7 +743,6 @@ macro_rules! bdk_blockchain_tests {
|
||||
|
||||
blockchain.broadcast(&tx1).expect("broadcasting first");
|
||||
blockchain.broadcast(&tx2).expect("broadcasting replacement");
|
||||
|
||||
receiver_wallet.sync(&blockchain, SyncOptions::default()).expect("syncing receiver");
|
||||
assert_eq!(receiver_wallet.get_balance().expect("balance"), 49_000, "should have received coins once and only once");
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ pub trait ConfigurableBlockchainTester<B: ConfigurableBlockchain>: Sized {
|
||||
|
||||
if self.config_with_stop_gap(test_client, 0).is_some() {
|
||||
test_wallet_sync_with_stop_gaps(test_client, self);
|
||||
test_wallet_sync_fulfills_missing_script_cache(test_client, self);
|
||||
test_wallet_sync_self_transfer_tx(test_client, self);
|
||||
} else {
|
||||
println!(
|
||||
"{}: Skipped tests requiring config_with_stop_gap.",
|
||||
@@ -113,16 +115,21 @@ where
|
||||
} else {
|
||||
max_balance
|
||||
};
|
||||
let details = format!(
|
||||
"test_vector: [stop_gap: {}, actual_gap: {}, addrs_before: {}, addrs_after: {}]",
|
||||
stop_gap, actual_gap, addrs_before, addrs_after,
|
||||
);
|
||||
println!("{}", details);
|
||||
|
||||
// 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,
|
||||
println!(
|
||||
"max: {}, min: {}, actual: {}",
|
||||
max_balance, min_balance, wallet_balance
|
||||
);
|
||||
|
||||
assert!(
|
||||
wallet_balance <= max_balance,
|
||||
"wallet balance is greater than received amount: {}",
|
||||
@@ -138,3 +145,113 @@ where
|
||||
test_client.generate(1, None);
|
||||
}
|
||||
}
|
||||
|
||||
/// With a `stop_gap` of x and every x addresses having a balance of 1000 (for y addresses),
|
||||
/// we expect `Wallet::sync` to correctly self-cache addresses, so that the resulting balance,
|
||||
/// after sync, should be y * 1000.
|
||||
fn test_wallet_sync_fulfills_missing_script_cache<T, B>(test_client: &mut TestClient, tester: &T)
|
||||
where
|
||||
T: ConfigurableBlockchainTester<B>,
|
||||
B: ConfigurableBlockchain,
|
||||
{
|
||||
// wallet descriptor
|
||||
let descriptor = "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/200/*)";
|
||||
|
||||
// amount in sats per tx
|
||||
const AMOUNT_PER_TX: u64 = 1000;
|
||||
|
||||
// addr constants
|
||||
const ADDR_COUNT: usize = 6;
|
||||
const ADDR_GAP: usize = 60;
|
||||
|
||||
let blockchain =
|
||||
B::from_config(&tester.config_with_stop_gap(test_client, ADDR_GAP).unwrap()).unwrap();
|
||||
|
||||
let wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap();
|
||||
|
||||
let expected_balance = (0..ADDR_COUNT).fold(0_u64, |sum, i| {
|
||||
let addr_i = i * ADDR_GAP;
|
||||
let address = wallet.get_address(AddressIndex::Peek(addr_i as _)).unwrap();
|
||||
|
||||
println!(
|
||||
"tx: {} sats => [{}] {}",
|
||||
AMOUNT_PER_TX,
|
||||
addr_i,
|
||||
address.to_string()
|
||||
);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@addr address.address) => AMOUNT_PER_TX )
|
||||
});
|
||||
test_client.generate(1, None);
|
||||
|
||||
sum + AMOUNT_PER_TX
|
||||
});
|
||||
println!("expected balance: {}, syncing...", expected_balance);
|
||||
|
||||
// perform sync
|
||||
wallet.sync(&blockchain, Default::default()).unwrap();
|
||||
println!("sync done!");
|
||||
|
||||
let balance = wallet.get_balance().unwrap();
|
||||
assert_eq!(balance, expected_balance);
|
||||
}
|
||||
|
||||
/// Given a `stop_gap`, a wallet with a 2 transactions, one sending to `scriptPubKey` at derivation
|
||||
/// index of `stop_gap`, and the other spending from the same `scriptPubKey` into another
|
||||
/// `scriptPubKey` at derivation index of `stop_gap * 2`, we expect `Wallet::sync` to perform
|
||||
/// correctly, so that we detect the total balance.
|
||||
fn test_wallet_sync_self_transfer_tx<T, B>(test_client: &mut TestClient, tester: &T)
|
||||
where
|
||||
T: ConfigurableBlockchainTester<B>,
|
||||
B: ConfigurableBlockchain,
|
||||
{
|
||||
const TRANSFER_AMOUNT: u64 = 10_000;
|
||||
const STOP_GAP: usize = 75;
|
||||
|
||||
let descriptor = "wpkh(tprv8i8F4EhYDMquzqiecEX8SKYMXqfmmb1Sm7deoA1Hokxzn281XgTkwsd6gL8aJevLE4aJugfVf9MKMvrcRvPawGMenqMBA3bRRfp4s1V7Eg3/*)";
|
||||
|
||||
let blockchain =
|
||||
B::from_config(&tester.config_with_stop_gap(test_client, STOP_GAP).unwrap()).unwrap();
|
||||
|
||||
let wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap();
|
||||
|
||||
let address1 = wallet
|
||||
.get_address(AddressIndex::Peek(STOP_GAP as _))
|
||||
.unwrap();
|
||||
let address2 = wallet
|
||||
.get_address(AddressIndex::Peek((STOP_GAP * 2) as _))
|
||||
.unwrap();
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@addr address1.address) => TRANSFER_AMOUNT )
|
||||
});
|
||||
test_client.generate(1, None);
|
||||
|
||||
wallet.sync(&blockchain, Default::default()).unwrap();
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(address2.script_pubkey(), TRANSFER_AMOUNT / 2);
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
assert!(wallet.sign(&mut psbt, Default::default()).unwrap());
|
||||
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
||||
|
||||
test_client.generate(1, None);
|
||||
|
||||
// obtain what is expected
|
||||
let fee = details.fee.unwrap();
|
||||
let expected_balance = TRANSFER_AMOUNT - fee;
|
||||
println!("fee={}, expected_balance={}", fee, expected_balance);
|
||||
|
||||
// actually test the wallet
|
||||
wallet.sync(&blockchain, Default::default()).unwrap();
|
||||
let balance = wallet.get_balance().unwrap();
|
||||
assert_eq!(balance, expected_balance);
|
||||
|
||||
// now try with a fresh wallet
|
||||
let fresh_wallet =
|
||||
Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap();
|
||||
fresh_wallet.sync(&blockchain, Default::default()).unwrap();
|
||||
let fresh_balance = fresh_wallet.get_balance().unwrap();
|
||||
assert_eq!(fresh_balance, expected_balance);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user