Skip to content

Commit e2814e8

Browse files
committed
fix: KeychainTxOutIndex recovery logic when spk-cache is enabled
We should ensure we populate the cache (from the changeset) before doing any other operation on `KeychainTxOutIndex` (i.e. inserting descriptors). Otherwise we will end up deriving spks which have already been cached. The test is also corrected to check that we don't duplicate deriving spks. A helper function `check_cache_cs` is added to make the tests more thorough and easier to read.
1 parent c39ce79 commit e2814e8

File tree

2 files changed

+155
-71
lines changed

2 files changed

+155
-71
lines changed

wallet/src/wallet/mod.rs

Lines changed: 67 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -471,27 +471,24 @@ impl Wallet {
471471
None => (None, Arc::new(SignersContainer::new())),
472472
};
473473

474-
let index = create_indexer(
474+
let mut stage = ChangeSet {
475+
descriptor: Some(descriptor.clone()),
476+
change_descriptor: change_descriptor.clone(),
477+
local_chain: chain_changeset,
478+
network: Some(network),
479+
..Default::default()
480+
};
481+
482+
let indexed_graph = make_indexed_graph(
483+
&mut stage,
484+
Default::default(),
485+
Default::default(),
475486
descriptor,
476487
change_descriptor,
477488
params.lookahead,
478489
params.use_spk_cache,
479490
)?;
480491

481-
let descriptor = index.get_descriptor(KeychainKind::External).cloned();
482-
let change_descriptor = index.get_descriptor(KeychainKind::Internal).cloned();
483-
let indexed_graph = IndexedTxGraph::new(index);
484-
let indexed_graph_changeset = indexed_graph.initial_changeset();
485-
486-
let stage = ChangeSet {
487-
descriptor,
488-
change_descriptor,
489-
local_chain: chain_changeset,
490-
tx_graph: indexed_graph_changeset.tx_graph,
491-
indexer: indexed_graph_changeset.indexer,
492-
network: Some(network),
493-
};
494-
495492
Ok(Wallet {
496493
signers,
497494
change_signers,
@@ -675,20 +672,19 @@ impl Wallet {
675672
None => Arc::new(SignersContainer::new()),
676673
};
677674

678-
let index = create_indexer(
675+
let mut stage = ChangeSet::default();
676+
677+
let indexed_graph = make_indexed_graph(
678+
&mut stage,
679+
changeset.tx_graph,
680+
changeset.indexer,
679681
descriptor,
680682
change_descriptor,
681683
params.lookahead,
682684
params.use_spk_cache,
683685
)
684686
.map_err(LoadError::Descriptor)?;
685687

686-
let mut indexed_graph = IndexedTxGraph::new(index);
687-
indexed_graph.apply_changeset(changeset.indexer.into());
688-
indexed_graph.apply_changeset(changeset.tx_graph.into());
689-
690-
let stage = ChangeSet::default();
691-
692688
Ok(Some(Wallet {
693689
signers,
694690
change_signers,
@@ -2656,35 +2652,61 @@ fn new_local_utxo(
26562652
}
26572653
}
26582654

2659-
fn create_indexer(
2655+
fn make_indexed_graph(
2656+
stage: &mut ChangeSet,
2657+
tx_graph_changeset: chain::tx_graph::ChangeSet<ConfirmationBlockTime>,
2658+
indexer_changeset: chain::keychain_txout::ChangeSet,
26602659
descriptor: ExtendedDescriptor,
26612660
change_descriptor: Option<ExtendedDescriptor>,
26622661
lookahead: u32,
26632662
use_spk_cache: bool,
2664-
) -> Result<KeychainTxOutIndex<KeychainKind>, DescriptorError> {
2665-
let mut indexer = KeychainTxOutIndex::<KeychainKind>::new(lookahead, use_spk_cache);
2666-
2667-
assert!(indexer
2668-
.insert_descriptor(KeychainKind::External, descriptor)
2669-
.expect("first descriptor introduced must succeed"));
2663+
) -> Result<IndexedTxGraph<ConfirmationBlockTime, KeychainTxOutIndex<KeychainKind>>, DescriptorError>
2664+
{
2665+
let (indexed_graph, changeset) = IndexedTxGraph::from_changeset(
2666+
chain::indexed_tx_graph::ChangeSet {
2667+
tx_graph: tx_graph_changeset,
2668+
indexer: indexer_changeset,
2669+
},
2670+
|idx_cs| -> Result<KeychainTxOutIndex<KeychainKind>, DescriptorError> {
2671+
let mut idx = KeychainTxOutIndex::from_changeset(lookahead, use_spk_cache, idx_cs);
2672+
2673+
let descriptor_inserted = idx
2674+
.insert_descriptor(KeychainKind::External, descriptor)
2675+
.expect("already checked to be a unique, wildcard, non-multipath descriptor");
2676+
assert!(
2677+
descriptor_inserted,
2678+
"this must be the first time we are seeing this descriptor"
2679+
);
2680+
2681+
let change_descriptor = match change_descriptor {
2682+
Some(change_descriptor) => change_descriptor,
2683+
None => return Ok(idx),
2684+
};
26702685

2671-
if let Some(change_descriptor) = change_descriptor {
2672-
assert!(indexer
2673-
.insert_descriptor(KeychainKind::Internal, change_descriptor)
2674-
.map_err(|e| {
2675-
use bdk_chain::indexer::keychain_txout::InsertDescriptorError;
2676-
match e {
2677-
InsertDescriptorError::DescriptorAlreadyAssigned { .. } => {
2678-
crate::descriptor::error::Error::ExternalAndInternalAreTheSame
2679-
}
2680-
InsertDescriptorError::KeychainAlreadyAssigned { .. } => {
2681-
unreachable!("this is the first time we're assigning internal")
2686+
let change_descriptor_inserted = idx
2687+
.insert_descriptor(KeychainKind::Internal, change_descriptor)
2688+
.map_err(|e| {
2689+
use bdk_chain::indexer::keychain_txout::InsertDescriptorError;
2690+
match e {
2691+
InsertDescriptorError::DescriptorAlreadyAssigned { .. } => {
2692+
crate::descriptor::error::Error::ExternalAndInternalAreTheSame
2693+
}
2694+
InsertDescriptorError::KeychainAlreadyAssigned { .. } => {
2695+
unreachable!("this is the first time we're assigning internal")
2696+
}
26822697
}
2683-
}
2684-
})?);
2685-
}
2686-
2687-
Ok(indexer)
2698+
})?;
2699+
assert!(
2700+
change_descriptor_inserted,
2701+
"this must be the first time we are seeing this descriptor"
2702+
);
2703+
2704+
Ok(idx)
2705+
},
2706+
)?;
2707+
stage.tx_graph.merge(changeset.tx_graph);
2708+
stage.indexer.merge(changeset.indexer);
2709+
Ok(indexed_graph)
26882710
}
26892711

26902712
/// Transforms a [`FeeRate`] to `f64` with unit as sat/vb.

wallet/tests/persisted_wallet.rs

Lines changed: 88 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use std::collections::BTreeMap;
1+
use std::collections::{BTreeMap, BTreeSet};
22
use std::path::Path;
33

44
use anyhow::Context;
55
use assert_matches::assert_matches;
6+
use bdk_chain::DescriptorId;
67
use bdk_chain::{
78
keychain_txout::DEFAULT_LOOKAHEAD, ChainPosition, ConfirmationBlockTime, DescriptorExt,
89
};
@@ -14,7 +15,9 @@ use bdk_wallet::{
1415
use bitcoin::constants::ChainHash;
1516
use bitcoin::hashes::Hash;
1617
use bitcoin::key::Secp256k1;
17-
use bitcoin::{absolute, transaction, Amount, BlockHash, Network, ScriptBuf, Transaction, TxOut};
18+
use bitcoin::{
19+
absolute, secp256k1, transaction, Amount, BlockHash, Network, ScriptBuf, Transaction, TxOut,
20+
};
1821
use miniscript::{Descriptor, DescriptorPublicKey};
1922

2023
mod common;
@@ -24,6 +27,57 @@ const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48];
2427

2528
#[test]
2629
fn wallet_is_persisted() -> anyhow::Result<()> {
30+
type SpkCacheChangeSet = BTreeMap<DescriptorId, BTreeMap<u32, ScriptBuf>>;
31+
32+
/// Check whether the spk-cache field of the changeset contains the expected spk indices.
33+
fn check_cache_cs(
34+
cache_cs: &SpkCacheChangeSet,
35+
expected: impl IntoIterator<Item = (KeychainKind, impl IntoIterator<Item = u32>)>,
36+
msg: impl AsRef<str>,
37+
) {
38+
let secp = secp256k1::Secp256k1::new();
39+
let (external, internal) = get_test_tr_single_sig_xprv_and_change_desc();
40+
let (external_desc, _) = external
41+
.into_wallet_descriptor(&secp, Network::Testnet)
42+
.unwrap();
43+
let (internal_desc, _) = internal
44+
.into_wallet_descriptor(&secp, Network::Testnet)
45+
.unwrap();
46+
let external_did = external_desc.descriptor_id();
47+
let internal_did = internal_desc.descriptor_id();
48+
49+
let cache_cmp = cache_cs
50+
.iter()
51+
.map(|(did, spks)| {
52+
let kind: KeychainKind;
53+
if did == &external_did {
54+
kind = KeychainKind::External;
55+
} else if did == &internal_did {
56+
kind = KeychainKind::Internal;
57+
} else {
58+
unreachable!();
59+
}
60+
let spk_indices = spks.keys().copied().collect::<BTreeSet<u32>>();
61+
(kind, spk_indices)
62+
})
63+
.filter(|(_, spk_indices)| !spk_indices.is_empty())
64+
.collect::<BTreeMap<KeychainKind, BTreeSet<u32>>>();
65+
66+
let expected_cmp = expected
67+
.into_iter()
68+
.map(|(kind, indices)| (kind, indices.into_iter().collect::<BTreeSet<u32>>()))
69+
.filter(|(_, spk_indices)| !spk_indices.is_empty())
70+
.collect::<BTreeMap<KeychainKind, BTreeSet<u32>>>();
71+
72+
assert_eq!(cache_cmp, expected_cmp, "{}", msg.as_ref());
73+
}
74+
75+
fn staged_cache(wallet: &Wallet) -> SpkCacheChangeSet {
76+
wallet.staged().map_or(SpkCacheChangeSet::default(), |cs| {
77+
cs.indexer.spk_cache.clone()
78+
})
79+
}
80+
2781
fn run<Db, CreateDb, OpenDb>(
2882
filename: &str,
2983
create_db: CreateDb,
@@ -46,8 +100,18 @@ fn wallet_is_persisted() -> anyhow::Result<()> {
46100
.network(Network::Testnet)
47101
.use_spk_cache(true)
48102
.create_wallet(&mut db)?;
103+
49104
wallet.reveal_next_address(KeychainKind::External);
50105

106+
check_cache_cs(
107+
&staged_cache(&wallet),
108+
[
109+
(KeychainKind::External, 0..DEFAULT_LOOKAHEAD + 1),
110+
(KeychainKind::Internal, 0..DEFAULT_LOOKAHEAD),
111+
],
112+
"cache cs must return initial set + the external index that was just derived",
113+
);
114+
51115
// persist new wallet changes
52116
assert!(wallet.persist(&mut db)?, "must write");
53117
wallet.spk_index().clone()
@@ -81,6 +145,7 @@ fn wallet_is_persisted() -> anyhow::Result<()> {
81145
.0
82146
);
83147
}
148+
84149
// Test SPK cache
85150
{
86151
let mut db = open_db(&file_path).context("failed to recover db")?;
@@ -90,35 +155,32 @@ fn wallet_is_persisted() -> anyhow::Result<()> {
90155
.load_wallet(&mut db)?
91156
.expect("wallet must exist");
92157

93-
let external_did = wallet
94-
.public_descriptor(KeychainKind::External)
95-
.descriptor_id();
96-
let internal_did = wallet
97-
.public_descriptor(KeychainKind::Internal)
98-
.descriptor_id();
99-
100158
assert!(wallet.staged().is_none());
101159

102-
let _addr = wallet.reveal_next_address(KeychainKind::External);
103-
let cs = wallet.staged().expect("we should have staged a changeset");
104-
assert!(!cs.indexer.spk_cache.is_empty(), "failed to cache spks");
105-
assert_eq!(cs.indexer.spk_cache.len(), 2, "we persisted two keychains");
106-
let spk_cache: &BTreeMap<u32, ScriptBuf> =
107-
cs.indexer.spk_cache.get(&external_did).unwrap();
108-
assert_eq!(spk_cache.len() as u32, 1 + 1 + DEFAULT_LOOKAHEAD);
109-
assert_eq!(spk_cache.keys().last(), Some(&26));
110-
let spk_cache = cs.indexer.spk_cache.get(&internal_did).unwrap();
111-
assert_eq!(spk_cache.len() as u32, DEFAULT_LOOKAHEAD);
112-
assert_eq!(spk_cache.keys().last(), Some(&24));
160+
let revealed_external_addr = wallet.reveal_next_address(KeychainKind::External);
161+
check_cache_cs(
162+
&staged_cache(&wallet),
163+
[(
164+
KeychainKind::External,
165+
[revealed_external_addr.index + DEFAULT_LOOKAHEAD],
166+
)],
167+
"must only persist the revealed+LOOKAHEAD indexed external spk",
168+
);
169+
113170
// Clear the stage
114171
let _ = wallet.take_staged();
115-
let _addr = wallet.reveal_next_address(KeychainKind::Internal);
116-
let cs = wallet.staged().unwrap();
117-
assert_eq!(cs.indexer.spk_cache.len(), 1);
118-
let spk_cache = cs.indexer.spk_cache.get(&internal_did).unwrap();
119-
assert_eq!(spk_cache.len(), 1);
120-
assert_eq!(spk_cache.keys().next(), Some(&25));
172+
173+
let revealed_internal_addr = wallet.reveal_next_address(KeychainKind::Internal);
174+
check_cache_cs(
175+
&staged_cache(&wallet),
176+
[(
177+
KeychainKind::Internal,
178+
[revealed_internal_addr.index + DEFAULT_LOOKAHEAD],
179+
)],
180+
"must only persist the revealed+LOOKAHEAD indexed internal spk",
181+
);
121182
}
183+
122184
// SPK cache requires load params
123185
{
124186
let mut db = open_db(&file_path).context("failed to recover db")?;

0 commit comments

Comments
 (0)