2023-09-04 00:20:45 -05:00
|
|
|
use bdk_esplora::EsploraExt;
|
|
|
|
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
|
|
|
use electrsd::bitcoind::{self, anyhow, BitcoinD};
|
|
|
|
use electrsd::{Conf, ElectrsD};
|
|
|
|
use esplora_client::{self, BlockingClient, Builder};
|
2023-11-20 11:52:30 +01:00
|
|
|
use std::collections::{BTreeMap, HashSet};
|
2023-09-04 00:20:45 -05:00
|
|
|
use std::str::FromStr;
|
|
|
|
use std::thread::sleep;
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
|
|
use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
|
|
|
|
|
|
|
|
struct TestEnv {
|
|
|
|
bitcoind: BitcoinD,
|
|
|
|
#[allow(dead_code)]
|
|
|
|
electrsd: ElectrsD,
|
|
|
|
client: BlockingClient,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TestEnv {
|
|
|
|
fn new() -> Result<Self, anyhow::Error> {
|
|
|
|
let bitcoind_exe =
|
|
|
|
bitcoind::downloaded_exe_path().expect("bitcoind version feature must be enabled");
|
|
|
|
let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
|
|
|
|
|
|
|
|
let mut electrs_conf = Conf::default();
|
|
|
|
electrs_conf.http_enabled = true;
|
|
|
|
let electrs_exe =
|
|
|
|
electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
|
|
|
|
let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &electrs_conf)?;
|
|
|
|
|
|
|
|
let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
|
|
|
|
let client = Builder::new(base_url.as_str()).build_blocking()?;
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
bitcoind,
|
|
|
|
electrsd,
|
|
|
|
client,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn mine_blocks(
|
|
|
|
&self,
|
|
|
|
count: usize,
|
|
|
|
address: Option<Address>,
|
|
|
|
) -> anyhow::Result<Vec<BlockHash>> {
|
|
|
|
let coinbase_address = match address {
|
|
|
|
Some(address) => address,
|
|
|
|
None => self
|
|
|
|
.bitcoind
|
|
|
|
.client
|
|
|
|
.get_new_address(None, None)?
|
|
|
|
.assume_checked(),
|
|
|
|
};
|
|
|
|
let block_hashes = self
|
|
|
|
.bitcoind
|
|
|
|
.client
|
|
|
|
.generate_to_address(count as _, &coinbase_address)?;
|
|
|
|
Ok(block_hashes)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
|
|
|
let env = TestEnv::new()?;
|
|
|
|
let receive_address0 =
|
|
|
|
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
|
|
|
let receive_address1 =
|
|
|
|
Address::from_str("bcrt1qfjg5lv3dvc9az8patec8fjddrs4aqtauadnagr")?.assume_checked();
|
|
|
|
|
|
|
|
let misc_spks = [
|
|
|
|
receive_address0.script_pubkey(),
|
|
|
|
receive_address1.script_pubkey(),
|
|
|
|
];
|
|
|
|
|
|
|
|
let _block_hashes = env.mine_blocks(101, None)?;
|
|
|
|
let txid1 = env.bitcoind.client.send_to_address(
|
|
|
|
&receive_address1,
|
|
|
|
Amount::from_sat(10000),
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
Some(1),
|
|
|
|
None,
|
|
|
|
)?;
|
|
|
|
let txid2 = env.bitcoind.client.send_to_address(
|
|
|
|
&receive_address0,
|
|
|
|
Amount::from_sat(20000),
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
Some(1),
|
|
|
|
None,
|
|
|
|
)?;
|
|
|
|
let _block_hashes = env.mine_blocks(1, None)?;
|
|
|
|
while env.client.get_height().unwrap() < 102 {
|
|
|
|
sleep(Duration::from_millis(10))
|
|
|
|
}
|
|
|
|
|
|
|
|
let graph_update = env.client.scan_txs(
|
|
|
|
misc_spks.into_iter(),
|
|
|
|
vec![].into_iter(),
|
|
|
|
vec![].into_iter(),
|
|
|
|
1,
|
|
|
|
)?;
|
|
|
|
|
|
|
|
let mut graph_update_txids: Vec<Txid> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
|
|
|
graph_update_txids.sort();
|
|
|
|
let mut expected_txids = vec![txid1, txid2];
|
|
|
|
expected_txids.sort();
|
|
|
|
assert_eq!(graph_update_txids, expected_txids);
|
2023-11-20 11:52:30 +01:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Test the bounds of the address scan depending on the gap limit.
|
|
|
|
#[test]
|
|
|
|
pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
|
|
|
let env = TestEnv::new()?;
|
|
|
|
let _block_hashes = env.mine_blocks(101, None)?;
|
|
|
|
|
|
|
|
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
|
|
|
let addresses = [
|
|
|
|
"bcrt1qj9f7r8r3p2y0sqf4r3r62qysmkuh0fzep473d2ar7rcz64wqvhssjgf0z4",
|
|
|
|
"bcrt1qmm5t0ch7vh2hryx9ctq3mswexcugqe4atkpkl2tetm8merqkthas3w7q30",
|
|
|
|
"bcrt1qut9p7ej7l7lhyvekj28xknn8gnugtym4d5qvnp5shrsr4nksmfqsmyn87g",
|
|
|
|
"bcrt1qqz0xtn3m235p2k96f5wa2dqukg6shxn9n3txe8arlrhjh5p744hsd957ww",
|
|
|
|
"bcrt1q9c0t62a8l6wfytmf2t9lfj35avadk3mm8g4p3l84tp6rl66m48sqrme7wu",
|
|
|
|
"bcrt1qkmh8yrk2v47cklt8dytk8f3ammcwa4q7dzattedzfhqzvfwwgyzsg59zrh",
|
|
|
|
"bcrt1qvgrsrzy07gjkkfr5luplt0azxtfwmwq5t62gum5jr7zwcvep2acs8hhnp2",
|
|
|
|
"bcrt1qw57edarcg50ansq8mk3guyrk78rk0fwvrds5xvqeupteu848zayq549av8",
|
|
|
|
"bcrt1qvtve5ekf6e5kzs68knvnt2phfw6a0yjqrlgat392m6zt9jsvyxhqfx67ef",
|
|
|
|
"bcrt1qw03ddumfs9z0kcu76ln7jrjfdwam20qtffmkcral3qtza90sp9kqm787uk",
|
|
|
|
];
|
|
|
|
let addresses: Vec<_> = addresses
|
|
|
|
.into_iter()
|
|
|
|
.map(|s| Address::from_str(s).unwrap().assume_checked())
|
|
|
|
.collect();
|
|
|
|
let spks: Vec<_> = addresses
|
|
|
|
.iter()
|
|
|
|
.enumerate()
|
|
|
|
.map(|(i, addr)| (i as u32, addr.script_pubkey()))
|
|
|
|
.collect();
|
|
|
|
let mut keychains = BTreeMap::new();
|
|
|
|
keychains.insert(0, spks);
|
|
|
|
|
|
|
|
// Then receive coins on the 4th address.
|
|
|
|
let txid_4th_addr = env.bitcoind.client.send_to_address(
|
|
|
|
&addresses[3],
|
|
|
|
Amount::from_sat(10000),
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
Some(1),
|
|
|
|
None,
|
|
|
|
)?;
|
|
|
|
let _block_hashes = env.mine_blocks(1, None)?;
|
|
|
|
while env.client.get_height().unwrap() < 103 {
|
|
|
|
sleep(Duration::from_millis(10))
|
|
|
|
}
|
|
|
|
|
|
|
|
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
|
|
|
|
// will.
|
|
|
|
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
|
|
|
|
keychains.clone(),
|
|
|
|
vec![].into_iter(),
|
|
|
|
vec![].into_iter(),
|
|
|
|
2,
|
|
|
|
1,
|
|
|
|
)?;
|
|
|
|
assert!(graph_update.full_txs().next().is_none());
|
|
|
|
assert!(active_indices.is_empty());
|
|
|
|
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
|
|
|
|
keychains.clone(),
|
|
|
|
vec![].into_iter(),
|
|
|
|
vec![].into_iter(),
|
|
|
|
3,
|
|
|
|
1,
|
|
|
|
)?;
|
|
|
|
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
|
|
|
assert_eq!(active_indices[&0], 3);
|
|
|
|
|
|
|
|
// Now receive a coin on the last address.
|
|
|
|
let txid_last_addr = env.bitcoind.client.send_to_address(
|
|
|
|
&addresses[addresses.len() - 1],
|
|
|
|
Amount::from_sat(10000),
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
Some(1),
|
|
|
|
None,
|
|
|
|
)?;
|
|
|
|
let _block_hashes = env.mine_blocks(1, None)?;
|
|
|
|
while env.client.get_height().unwrap() < 104 {
|
|
|
|
sleep(Duration::from_millis(10))
|
|
|
|
}
|
|
|
|
|
|
|
|
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
|
|
|
|
// The last active indice won't be updated in the first case but will in the second one.
|
|
|
|
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
|
|
|
|
keychains.clone(),
|
|
|
|
vec![].into_iter(),
|
|
|
|
vec![].into_iter(),
|
|
|
|
4,
|
|
|
|
1,
|
|
|
|
)?;
|
|
|
|
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
|
|
|
assert_eq!(txs.len(), 1);
|
|
|
|
assert!(txs.contains(&txid_4th_addr));
|
|
|
|
assert_eq!(active_indices[&0], 3);
|
|
|
|
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
|
|
|
|
keychains,
|
|
|
|
vec![].into_iter(),
|
|
|
|
vec![].into_iter(),
|
|
|
|
5,
|
|
|
|
1,
|
|
|
|
)?;
|
|
|
|
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
|
|
|
assert_eq!(txs.len(), 2);
|
|
|
|
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
|
|
|
assert_eq!(active_indices[&0], 9);
|
|
|
|
|
2023-09-04 00:20:45 -05:00
|
|
|
Ok(())
|
|
|
|
}
|