Skip to content

Commit 2f9249b

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 25c8b37 commit 2f9249b

File tree

2 files changed

+158
-75
lines changed

2 files changed

+158
-75
lines changed

wallet/src/wallet/mod.rs

Lines changed: 70 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -477,31 +477,27 @@ impl Wallet {
477477
None => (None, Arc::new(SignersContainer::new())),
478478
};
479479

480-
let index = create_indexer(
481-
descriptor,
482-
change_descriptor,
483-
params.lookahead,
484-
params.use_spk_cache,
485-
)?;
486-
487-
let descriptor = index.get_descriptor(KeychainKind::External).cloned();
488-
let change_descriptor = index.get_descriptor(KeychainKind::Internal).cloned();
489-
let indexed_graph = IndexedTxGraph::new(index);
490-
let indexed_graph_changeset = indexed_graph.initial_changeset();
491-
492480
let intent_tracker = IntentTracker::default();
493-
let intent_tracker_changeset = intent_tracker.initial_changeset();
494481

495-
let stage = ChangeSet {
496-
descriptor,
497-
change_descriptor,
482+
let mut stage = ChangeSet {
483+
descriptor: Some(descriptor.clone()),
484+
change_descriptor: change_descriptor.clone(),
498485
local_chain: chain_changeset,
499-
tx_graph: indexed_graph_changeset.tx_graph,
500-
indexer: indexed_graph_changeset.indexer,
501-
intent_tracker: intent_tracker_changeset,
486+
intent_tracker: intent_tracker.initial_changeset(),
502487
network: Some(network),
488+
..Default::default()
503489
};
504490

491+
let indexed_graph = make_indexed_graph(
492+
&mut stage,
493+
Default::default(),
494+
Default::default(),
495+
descriptor,
496+
change_descriptor,
497+
params.lookahead,
498+
params.use_spk_cache,
499+
)?;
500+
505501
let mut wallet = Wallet {
506502
signers,
507503
change_signers,
@@ -691,22 +687,21 @@ impl Wallet {
691687
None => Arc::new(SignersContainer::new()),
692688
};
693689

694-
let index = create_indexer(
690+
let mut stage = ChangeSet::default();
691+
692+
let indexed_graph = make_indexed_graph(
693+
&mut stage,
694+
changeset.tx_graph,
695+
changeset.indexer,
695696
descriptor,
696697
change_descriptor,
697698
params.lookahead,
698699
params.use_spk_cache,
699700
)
700701
.map_err(LoadError::Descriptor)?;
701702

702-
let mut indexed_graph = IndexedTxGraph::new(index);
703-
indexed_graph.apply_changeset(changeset.indexer.into());
704-
indexed_graph.apply_changeset(changeset.tx_graph.into());
705-
706703
let broadcast_queue = IntentTracker::from_changeset(changeset.intent_tracker);
707704

708-
let stage = ChangeSet::default();
709-
710705
let mut wallet = Wallet {
711706
signers,
712707
change_signers,
@@ -2957,35 +2952,61 @@ fn new_local_utxo(
29572952
}
29582953
}
29592954

2960-
fn create_indexer(
2955+
fn make_indexed_graph(
2956+
stage: &mut ChangeSet,
2957+
tx_graph_changeset: chain::tx_graph::ChangeSet<ConfirmationBlockTime>,
2958+
indexer_changeset: chain::keychain_txout::ChangeSet,
29612959
descriptor: ExtendedDescriptor,
29622960
change_descriptor: Option<ExtendedDescriptor>,
29632961
lookahead: u32,
29642962
use_spk_cache: bool,
2965-
) -> Result<KeychainTxOutIndex<KeychainKind>, DescriptorError> {
2966-
let mut indexer = KeychainTxOutIndex::<KeychainKind>::new(lookahead, use_spk_cache);
2967-
2968-
assert!(indexer
2969-
.insert_descriptor(KeychainKind::External, descriptor)
2970-
.expect("first descriptor introduced must succeed"));
2963+
) -> Result<IndexedTxGraph<ConfirmationBlockTime, KeychainTxOutIndex<KeychainKind>>, DescriptorError>
2964+
{
2965+
let (indexed_graph, changeset) = IndexedTxGraph::from_changeset(
2966+
chain::indexed_tx_graph::ChangeSet {
2967+
tx_graph: tx_graph_changeset,
2968+
indexer: indexer_changeset,
2969+
},
2970+
|idx_cs| -> Result<KeychainTxOutIndex<KeychainKind>, DescriptorError> {
2971+
let mut idx = KeychainTxOutIndex::from_changeset(lookahead, use_spk_cache, idx_cs);
2972+
2973+
let descriptor_inserted = idx
2974+
.insert_descriptor(KeychainKind::External, descriptor)
2975+
.expect("already checked to be a unique, wildcard, non-multipath descriptor");
2976+
assert!(
2977+
descriptor_inserted,
2978+
"this must be the first time we are seeing this descriptor"
2979+
);
2980+
2981+
let change_descriptor = match change_descriptor {
2982+
Some(change_descriptor) => change_descriptor,
2983+
None => return Ok(idx),
2984+
};
29712985

2972-
if let Some(change_descriptor) = change_descriptor {
2973-
assert!(indexer
2974-
.insert_descriptor(KeychainKind::Internal, change_descriptor)
2975-
.map_err(|e| {
2976-
use bdk_chain::indexer::keychain_txout::InsertDescriptorError;
2977-
match e {
2978-
InsertDescriptorError::DescriptorAlreadyAssigned { .. } => {
2979-
crate::descriptor::error::Error::ExternalAndInternalAreTheSame
2980-
}
2981-
InsertDescriptorError::KeychainAlreadyAssigned { .. } => {
2982-
unreachable!("this is the first time we're assigning internal")
2986+
let change_descriptor_inserted = idx
2987+
.insert_descriptor(KeychainKind::Internal, change_descriptor)
2988+
.map_err(|e| {
2989+
use bdk_chain::indexer::keychain_txout::InsertDescriptorError;
2990+
match e {
2991+
InsertDescriptorError::DescriptorAlreadyAssigned { .. } => {
2992+
crate::descriptor::error::Error::ExternalAndInternalAreTheSame
2993+
}
2994+
InsertDescriptorError::KeychainAlreadyAssigned { .. } => {
2995+
unreachable!("this is the first time we're assigning internal")
2996+
}
29832997
}
2984-
}
2985-
})?);
2986-
}
2987-
2988-
Ok(indexer)
2998+
})?;
2999+
assert!(
3000+
change_descriptor_inserted,
3001+
"this must be the first time we are seeing this descriptor"
3002+
);
3003+
3004+
Ok(idx)
3005+
},
3006+
)?;
3007+
stage.tx_graph.merge(changeset.tx_graph);
3008+
stage.indexer.merge(changeset.indexer);
3009+
Ok(indexed_graph)
29893010
}
29903011

29913012
/// 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,11 +1,12 @@
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;
66
use bdk_chain::{
77
keychain_txout::DEFAULT_LOOKAHEAD, ChainPosition, ConfirmationBlockTime, DescriptorExt,
88
};
9+
use bdk_chain::{DescriptorId, KeychainIndexed};
910
use bdk_wallet::descriptor::IntoWalletDescriptor;
1011
use bdk_wallet::test_utils::*;
1112
use bdk_wallet::{
@@ -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)