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).
258 lines
9.2 KiB
Rust
258 lines
9.2 KiB
Rust
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);
|
|
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.",
|
|
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
|
|
};
|
|
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();
|
|
println!(
|
|
"max: {}, min: {}, actual: {}",
|
|
max_balance, min_balance, wallet_balance
|
|
);
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// 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);
|
|
}
|