Skip to content

Commit 7f40b38

Browse files
committed
test: add tests to check WalletPersister impl
1 parent f6b7d15 commit 7f40b38

File tree

2 files changed

+325
-0
lines changed

2 files changed

+325
-0
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ wallet = ["bdk_wallet"]
2323
[dev-dependencies]
2424
anyhow = "1.0.98"
2525
bdk_testenv = { version = "0.13.0" }
26+
bdk_wallet = {version = "2.0.0", features = ["test-utils"]}
27+
assert_matches = "1.5.0"
2628

2729
[lints.rust]
2830
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage,coverage_nightly)'] }

src/lib.rs

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,20 @@ mod test {
897897

898898
use bdk_testenv::{block_id, hash, utils};
899899

900+
#[cfg(feature = "wallet")]
901+
use bdk_wallet::{
902+
KeychainKind, LoadError, LoadMismatch, LoadWithPersistError, Wallet,
903+
bitcoin::{constants::ChainHash, key::Secp256k1},
904+
descriptor::IntoWalletDescriptor,
905+
test_utils::{
906+
get_test_tr_single_sig_xprv, get_test_tr_single_sig_xprv_and_change_desc,
907+
get_test_wpkh, insert_anchor, insert_tx,
908+
},
909+
};
910+
911+
#[cfg(feature = "wallet")]
912+
use assert_matches::assert_matches;
913+
900914
#[cfg(feature = "wallet")]
901915
const DESCRIPTORS: [&str; 4] = [
902916
"tr([5940b9b9/86'/0'/0']tpubDDVNqmq75GNPWQ9UNKfP43UwjaHU4GYfoPavojQbfpyfZp2KetWgjGBRRAy4tYCrAA6SB11mhQAkqxjh1VtQHyKwT4oYxpwLaGHvoKmtxZf/1/*)#ypcpw2dr",
@@ -2159,4 +2173,313 @@ mod test {
21592173
store2.read_wallet(&mut changeset_read).unwrap();
21602174
assert_eq!(changeset_read, changeset2);
21612175
}
2176+
2177+
#[cfg(feature = "wallet")]
2178+
#[test]
2179+
fn wallet_is_persisted() {
2180+
use bdk_chain::keychain_txout::DEFAULT_LOOKAHEAD;
2181+
2182+
let tmpfile = NamedTempFile::new().unwrap();
2183+
let db = Arc::new(create_db(tmpfile.path()));
2184+
let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_and_change_desc();
2185+
2186+
// create new wallet
2187+
let wallet_spk_index = {
2188+
let mut store = create_test_store(db.clone(), "wallet");
2189+
let mut wallet = Wallet::create(external_desc, internal_desc)
2190+
.network(Network::Testnet)
2191+
.use_spk_cache(true)
2192+
.create_wallet(&mut store)
2193+
.expect("should create wallet");
2194+
wallet.reveal_next_address(KeychainKind::External);
2195+
2196+
// persist new wallet changes
2197+
assert!(
2198+
wallet.persist(&mut store).expect("should persist"),
2199+
"must write"
2200+
);
2201+
wallet.spk_index().clone()
2202+
};
2203+
2204+
// recover wallet
2205+
{
2206+
let mut store = create_test_store(db.clone(), "wallet");
2207+
let wallet = Wallet::load()
2208+
.descriptor(KeychainKind::External, Some(external_desc))
2209+
.descriptor(KeychainKind::Internal, Some(internal_desc))
2210+
.check_network(Network::Testnet)
2211+
.load_wallet(&mut store)
2212+
.expect("should load wallet")
2213+
.expect("wallet must exist");
2214+
2215+
assert_eq!(wallet.network(), Network::Testnet);
2216+
assert_eq!(
2217+
wallet.spk_index().keychains().collect::<Vec<_>>(),
2218+
wallet_spk_index.keychains().collect::<Vec<_>>()
2219+
);
2220+
assert_eq!(
2221+
wallet.spk_index().last_revealed_indices(),
2222+
wallet_spk_index.last_revealed_indices()
2223+
);
2224+
let secp = Secp256k1::new();
2225+
assert_eq!(
2226+
*wallet.public_descriptor(KeychainKind::External),
2227+
external_desc
2228+
.into_wallet_descriptor(&secp, wallet.network())
2229+
.unwrap()
2230+
.0
2231+
);
2232+
}
2233+
// Test SPK cache
2234+
{
2235+
let mut store = create_test_store(db.clone(), "wallet");
2236+
let mut wallet = Wallet::load()
2237+
.check_network(Network::Testnet)
2238+
.use_spk_cache(true)
2239+
.load_wallet(&mut store)
2240+
.expect("should load wallet")
2241+
.expect("wallet must exist");
2242+
2243+
let external_did = wallet
2244+
.public_descriptor(KeychainKind::External)
2245+
.descriptor_id();
2246+
let internal_did = wallet
2247+
.public_descriptor(KeychainKind::Internal)
2248+
.descriptor_id();
2249+
2250+
assert!(wallet.staged().is_none());
2251+
2252+
let _addr = wallet.reveal_next_address(KeychainKind::External);
2253+
let cs = wallet.staged().expect("we should have staged a changeset");
2254+
assert!(!cs.indexer.spk_cache.is_empty(), "failed to cache spks");
2255+
assert_eq!(cs.indexer.spk_cache.len(), 2, "we persisted two keychains");
2256+
let spk_cache: &BTreeMap<u32, ScriptBuf> =
2257+
cs.indexer.spk_cache.get(&external_did).unwrap();
2258+
assert_eq!(spk_cache.len() as u32, 1 + 1 + DEFAULT_LOOKAHEAD);
2259+
assert_eq!(spk_cache.keys().last(), Some(&26));
2260+
let spk_cache = cs.indexer.spk_cache.get(&internal_did).unwrap();
2261+
assert_eq!(spk_cache.len() as u32, DEFAULT_LOOKAHEAD);
2262+
assert_eq!(spk_cache.keys().last(), Some(&24));
2263+
// Clear the stage
2264+
let _ = wallet.take_staged();
2265+
let _addr = wallet.reveal_next_address(KeychainKind::Internal);
2266+
let cs = wallet.staged().unwrap();
2267+
assert_eq!(cs.indexer.spk_cache.len(), 1);
2268+
let spk_cache = cs.indexer.spk_cache.get(&internal_did).unwrap();
2269+
assert_eq!(spk_cache.len(), 1);
2270+
assert_eq!(spk_cache.keys().next(), Some(&25));
2271+
}
2272+
// SPK cache requires load params
2273+
{
2274+
let mut store = create_test_store(db.clone(), "wallet");
2275+
let mut wallet = Wallet::load()
2276+
.check_network(Network::Testnet)
2277+
// .use_spk_cache(false)
2278+
.load_wallet(&mut store)
2279+
.expect("should load wallet")
2280+
.expect("wallet must exist");
2281+
2282+
let internal_did = wallet
2283+
.public_descriptor(KeychainKind::Internal)
2284+
.descriptor_id();
2285+
2286+
assert!(wallet.staged().is_none());
2287+
2288+
let _addr = wallet.reveal_next_address(KeychainKind::Internal);
2289+
let cs = wallet.staged().expect("we should have staged a changeset");
2290+
assert_eq!(cs.indexer.last_revealed.get(&internal_did), Some(&0));
2291+
assert!(
2292+
cs.indexer.spk_cache.is_empty(),
2293+
"we didn't set `use_spk_cache`"
2294+
);
2295+
}
2296+
}
2297+
2298+
#[cfg(feature = "wallet")]
2299+
#[test]
2300+
fn wallet_load_checks() {
2301+
let tmpfile = NamedTempFile::new().unwrap();
2302+
let db = Arc::new(create_db(tmpfile.path()));
2303+
let network = Network::Testnet;
2304+
let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_and_change_desc();
2305+
2306+
// create new wallet
2307+
2308+
let _ = Wallet::create(external_desc, internal_desc)
2309+
.network(network)
2310+
.create_wallet(&mut create_test_store(db.clone(), "wallet"))
2311+
.expect("wallet should get created");
2312+
2313+
assert_matches!(
2314+
Wallet::load()
2315+
.check_network(Network::Regtest)
2316+
.load_wallet(&mut create_test_store(db.clone(), "wallet")),
2317+
Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(
2318+
LoadMismatch::Network {
2319+
loaded: Network::Testnet,
2320+
expected: Network::Regtest,
2321+
}
2322+
))),
2323+
"unexpected network check result: Regtest (check) is not Testnet (loaded)",
2324+
);
2325+
2326+
let mainnet_hash = BlockHash::from_byte_array(ChainHash::BITCOIN.to_bytes());
2327+
assert_matches!(
2328+
Wallet::load()
2329+
.check_genesis_hash(mainnet_hash)
2330+
.load_wallet(&mut create_test_store(db.clone(), "wallet")),
2331+
Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(
2332+
LoadMismatch::Genesis { .. }
2333+
))),
2334+
"unexpected genesis hash check result: mainnet hash (check) is not testnet hash (loaded)",
2335+
);
2336+
assert_matches!(
2337+
Wallet::load()
2338+
.descriptor(KeychainKind::External, Some(internal_desc))
2339+
.load_wallet(&mut create_test_store(db.clone(), "wallet")),
2340+
Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(
2341+
LoadMismatch::Descriptor { .. }
2342+
))),
2343+
"unexpected descriptors check result",
2344+
);
2345+
assert_matches!(
2346+
Wallet::load()
2347+
.descriptor(KeychainKind::External, Option::<&str>::None)
2348+
.load_wallet(&mut create_test_store(db.clone(), "wallet")),
2349+
Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(
2350+
LoadMismatch::Descriptor { .. }
2351+
))),
2352+
"unexpected descriptors check result",
2353+
);
2354+
// check setting keymaps
2355+
let (_, external_keymap) =
2356+
<Descriptor<DescriptorPublicKey>>::parse_descriptor(&Secp256k1::new(), external_desc)
2357+
.expect("failed to parse descriptor");
2358+
let (_, internal_keymap) =
2359+
<Descriptor<DescriptorPublicKey>>::parse_descriptor(&Secp256k1::new(), internal_desc)
2360+
.expect("failed to parse descriptor");
2361+
let wallet = Wallet::load()
2362+
.keymap(KeychainKind::External, external_keymap)
2363+
.keymap(KeychainKind::Internal, internal_keymap)
2364+
.load_wallet(&mut create_test_store(db.clone(), "wallet"))
2365+
.expect("db should not fail")
2366+
.expect("wallet was persisted");
2367+
for keychain in [KeychainKind::External, KeychainKind::Internal] {
2368+
let keymap = wallet.get_signers(keychain).as_key_map(wallet.secp_ctx());
2369+
assert!(
2370+
!keymap.is_empty(),
2371+
"load should populate keymap for keychain {keychain:?}"
2372+
);
2373+
}
2374+
}
2375+
2376+
#[cfg(feature = "wallet")]
2377+
#[test]
2378+
fn wallet_should_persist_anchors_and_recover() {
2379+
use bdk_chain::ChainPosition;
2380+
let tmpfile = NamedTempFile::new().unwrap();
2381+
let db = Arc::new(create_db(tmpfile.path()));
2382+
let mut store = create_test_store(db.clone(), "wallet");
2383+
2384+
let desc = get_test_tr_single_sig_xprv();
2385+
let mut wallet = Wallet::create_single(desc)
2386+
.network(Network::Testnet)
2387+
.create_wallet(&mut store)
2388+
.unwrap();
2389+
2390+
let small_output_tx = Transaction {
2391+
input: vec![],
2392+
output: vec![TxOut {
2393+
script_pubkey: wallet
2394+
.next_unused_address(KeychainKind::External)
2395+
.script_pubkey(),
2396+
value: Amount::from_sat(25_000),
2397+
}],
2398+
version: transaction::Version::non_standard(0),
2399+
lock_time: absolute::LockTime::ZERO,
2400+
};
2401+
let txid = small_output_tx.compute_txid();
2402+
insert_tx(&mut wallet, small_output_tx);
2403+
let expected_anchor = ConfirmationBlockTime {
2404+
block_id: wallet.latest_checkpoint().block_id(),
2405+
confirmation_time: 200,
2406+
};
2407+
insert_anchor(&mut wallet, txid, expected_anchor);
2408+
assert!(wallet.persist(&mut store).unwrap());
2409+
2410+
// should recover persisted wallet
2411+
let secp = wallet.secp_ctx();
2412+
let (_, keymap) = <Descriptor<DescriptorPublicKey>>::parse_descriptor(secp, desc).unwrap();
2413+
assert!(!keymap.is_empty());
2414+
let wallet = Wallet::load()
2415+
.descriptor(KeychainKind::External, Some(desc))
2416+
.extract_keys()
2417+
.load_wallet(&mut store)
2418+
.unwrap()
2419+
.expect("must have loaded changeset");
2420+
// stored anchor should be retrieved in the same condition it was persisted
2421+
if let ChainPosition::Confirmed {
2422+
anchor: obtained_anchor,
2423+
..
2424+
} = wallet
2425+
.get_tx(txid)
2426+
.expect("should retrieve stored tx")
2427+
.chain_position
2428+
{
2429+
assert_eq!(obtained_anchor, expected_anchor)
2430+
} else {
2431+
panic!("Should have got ChainPosition::Confirmed)");
2432+
}
2433+
}
2434+
2435+
#[cfg(feature = "wallet")]
2436+
#[test]
2437+
fn single_descriptor_wallet_persist_and_recover() {
2438+
use bdk_chain::miniscript::Descriptor;
2439+
use bdk_chain::miniscript::DescriptorPublicKey;
2440+
2441+
let tmpfile = NamedTempFile::new().unwrap();
2442+
let db = Arc::new(create_db(tmpfile.path()));
2443+
let mut store = create_test_store(db.clone(), "wallet");
2444+
2445+
let desc = get_test_tr_single_sig_xprv();
2446+
let mut wallet = Wallet::create_single(desc)
2447+
.network(Network::Testnet)
2448+
.create_wallet(&mut store)
2449+
.unwrap();
2450+
let _ = wallet.reveal_addresses_to(KeychainKind::External, 2);
2451+
assert!(wallet.persist(&mut store).unwrap());
2452+
2453+
// should recover persisted wallet
2454+
let secp = wallet.secp_ctx();
2455+
let (_, keymap) = <Descriptor<DescriptorPublicKey>>::parse_descriptor(secp, desc).unwrap();
2456+
assert!(!keymap.is_empty());
2457+
let wallet = Wallet::load()
2458+
.descriptor(KeychainKind::External, Some(desc))
2459+
.extract_keys()
2460+
.load_wallet(&mut store)
2461+
.unwrap()
2462+
.expect("must have loaded changeset");
2463+
assert_eq!(wallet.derivation_index(KeychainKind::External), Some(2));
2464+
// should have private key
2465+
assert_eq!(
2466+
wallet.get_signers(KeychainKind::External).as_key_map(secp),
2467+
keymap,
2468+
);
2469+
2470+
// should error on wrong internal params
2471+
let desc = get_test_wpkh();
2472+
let (exp_desc, _) =
2473+
<Descriptor<DescriptorPublicKey>>::parse_descriptor(secp, desc).unwrap();
2474+
let err = Wallet::load()
2475+
.descriptor(KeychainKind::Internal, Some(desc))
2476+
.extract_keys()
2477+
.load_wallet(&mut store);
2478+
assert_matches!(
2479+
err,
2480+
Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(LoadMismatch::Descriptor { keychain, loaded, expected })))
2481+
if keychain == KeychainKind::Internal && loaded.is_none() && expected == Some(exp_desc),
2482+
"single descriptor wallet should refuse change descriptor param"
2483+
);
2484+
}
21622485
}

0 commit comments

Comments
 (0)