Skip to content

Commit 270830a

Browse files
committed
feat(wallet): maintain derived-account rolling lookahead
1 parent 54a6596 commit 270830a

8 files changed

Lines changed: 425 additions & 70 deletions

File tree

crates/bdk-wallet-aggregate/src/lib.rs

Lines changed: 141 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,39 @@ where
746746
Ok(wallet.start_sync_with_revealed_spks().build())
747747
}
748748

749+
/// Build an incremental sync request including external lookahead scripts.
750+
///
751+
/// The wallet's configured lookahead determines how many unrevealed scripts are included.
752+
pub fn wallet_incremental_sync_request_with_lookahead(
753+
&self, key: &K,
754+
) -> Result<SyncRequest<(KeychainKind, u32)>, Error> {
755+
let wallet = self.wallets.get(key).ok_or(Error::WalletNotFound)?;
756+
let mut request = wallet.start_sync_with_revealed_spks();
757+
let external_lookahead = wallet.spk_index().lookahead();
758+
if external_lookahead == 0 {
759+
return Ok(request.build());
760+
}
761+
762+
let start_index = match wallet.derivation_index(KeychainKind::External) {
763+
Some(BIP32_MAX_NORMAL_INDEX) => return Ok(request.build()),
764+
Some(index) => index + 1,
765+
None => 0,
766+
};
767+
let end_index =
768+
start_index.saturating_add(external_lookahead - 1).min(BIP32_MAX_NORMAL_INDEX);
769+
let lookahead_spks = (start_index..=end_index)
770+
.map(|index| {
771+
wallet
772+
.spk_index()
773+
.spk_at_index(KeychainKind::External, index)
774+
.map(|spk| ((KeychainKind::External, index), spk))
775+
.ok_or(Error::WalletOperationFailed)
776+
})
777+
.collect::<Result<Vec<_>, _>>()?;
778+
request = request.spks_with_indexes(lookahead_spks);
779+
Ok(request.build())
780+
}
781+
749782
/// Apply a chain update to the primary wallet.
750783
///
751784
/// Returns the wallet events and a list of all transaction IDs in the
@@ -1698,31 +1731,52 @@ mod tests {
16981731
P: WalletPersister,
16991732
P::Error: std::fmt::Debug,
17001733
{
1701-
PersistedWallet::create(
1702-
persister,
1703-
Wallet::create(
1704-
Bip84(xprv, KeychainKind::External),
1705-
Bip84(xprv, KeychainKind::Internal),
1706-
)
1707-
.network(Network::Regtest),
1734+
create_empty_wallet_from_xprv_with_lookahead(persister, xprv, None)
1735+
}
1736+
1737+
fn create_empty_wallet_from_xprv_with_lookahead<P>(
1738+
persister: &mut P, xprv: Xpriv, lookahead: Option<u32>,
1739+
) -> PersistedWallet<P>
1740+
where
1741+
P: WalletPersister,
1742+
P::Error: std::fmt::Debug,
1743+
{
1744+
let mut params = Wallet::create(
1745+
Bip84(xprv, KeychainKind::External),
1746+
Bip84(xprv, KeychainKind::Internal),
17081747
)
1709-
.unwrap()
1748+
.network(Network::Regtest);
1749+
if let Some(lookahead) = lookahead {
1750+
params = params.lookahead(lookahead);
1751+
}
1752+
PersistedWallet::create(persister, params).unwrap()
17101753
}
17111754

17121755
fn load_empty_wallet<P>(persister: &mut P) -> PersistedWallet<P>
1756+
where
1757+
P: WalletPersister,
1758+
P::Error: std::fmt::Debug,
1759+
{
1760+
load_empty_wallet_with_lookahead(persister, None)
1761+
}
1762+
1763+
fn load_empty_wallet_with_lookahead<P>(
1764+
persister: &mut P, lookahead: Option<u32>,
1765+
) -> PersistedWallet<P>
17131766
where
17141767
P: WalletPersister,
17151768
P::Error: std::fmt::Debug,
17161769
{
17171770
let xprv = test_xprv();
1718-
Wallet::load()
1771+
let mut params = Wallet::load()
17191772
.descriptor(KeychainKind::External, Some(Bip84(xprv, KeychainKind::External)))
17201773
.descriptor(KeychainKind::Internal, Some(Bip84(xprv, KeychainKind::Internal)))
17211774
.extract_keys()
1722-
.check_network(Network::Regtest)
1723-
.load_wallet(persister)
1724-
.unwrap()
1725-
.expect("wallet should have been persisted")
1775+
.check_network(Network::Regtest);
1776+
if let Some(lookahead) = lookahead {
1777+
params = params.lookahead(lookahead);
1778+
}
1779+
params.load_wallet(persister).unwrap().expect("wallet should have been persisted")
17261780
}
17271781

17281782
#[test]
@@ -2283,6 +2337,80 @@ mod tests {
22832337
assert_ne!(next.address, peeked.address);
22842338
}
22852339

2340+
#[test]
2341+
fn incremental_lookahead_does_not_advance_receive_index() {
2342+
let mut persister = NoopPersister;
2343+
let wallet =
2344+
create_empty_wallet_from_xprv_with_lookahead(&mut persister, test_xprv(), Some(3));
2345+
let mut aggregate = AggregateWallet::<u8, _>::new(wallet, persister, 0, vec![]);
2346+
2347+
let request = aggregate.wallet_incremental_sync_request_with_lookahead(&0).unwrap();
2348+
assert_eq!(request.progress().spks_remaining, 3);
2349+
assert_eq!(aggregate.new_address_info_for(&0).unwrap().index, 0);
2350+
}
2351+
2352+
#[test]
2353+
fn incremental_lookahead_rolls_forward_after_activity() {
2354+
let mut persister = NoopPersister;
2355+
let wallet =
2356+
create_empty_wallet_from_xprv_with_lookahead(&mut persister, test_xprv(), Some(3));
2357+
let mut aggregate = AggregateWallet::<u8, _>::new(wallet, persister, 0, vec![]);
2358+
let address = aggregate.address_info_for(&0, KeychainKind::External, 2).unwrap().address;
2359+
let transaction = Transaction {
2360+
version: bitcoin::transaction::Version::TWO,
2361+
lock_time: LockTime::ZERO,
2362+
input: vec![TxIn {
2363+
previous_output: OutPoint { txid: Txid::from_byte_array([0x18; 32]), vout: 0 },
2364+
..Default::default()
2365+
}],
2366+
output: vec![TxOut {
2367+
value: Amount::from_sat(50_000),
2368+
script_pubkey: address.script_pubkey(),
2369+
}],
2370+
};
2371+
2372+
aggregate.apply_mempool_txs(vec![(transaction, 0)], vec![]).unwrap();
2373+
2374+
let request = aggregate.wallet_incremental_sync_request_with_lookahead(&0).unwrap();
2375+
assert_eq!(request.progress().spks_remaining, 6);
2376+
assert_eq!(aggregate.new_address_info_for(&0).unwrap().index, 3);
2377+
}
2378+
2379+
#[test]
2380+
fn incremental_lookahead_activity_persists_the_active_index() {
2381+
let mut create_persister = MemoryPersister::default();
2382+
let wallet = create_empty_wallet_from_xprv_with_lookahead(
2383+
&mut create_persister,
2384+
test_xprv(),
2385+
Some(3),
2386+
);
2387+
let aggregate_persister = create_persister.clone();
2388+
{
2389+
let mut aggregate =
2390+
AggregateWallet::<u8, _>::new(wallet, aggregate_persister, 0, vec![]);
2391+
let address =
2392+
aggregate.address_info_for(&0, KeychainKind::External, 2).unwrap().address;
2393+
let transaction = Transaction {
2394+
version: bitcoin::transaction::Version::TWO,
2395+
lock_time: LockTime::ZERO,
2396+
input: vec![TxIn {
2397+
previous_output: OutPoint { txid: Txid::from_byte_array([0x19; 32]), vout: 0 },
2398+
..Default::default()
2399+
}],
2400+
output: vec![TxOut {
2401+
value: Amount::from_sat(50_000),
2402+
script_pubkey: address.script_pubkey(),
2403+
}],
2404+
};
2405+
aggregate.apply_mempool_txs(vec![(transaction, 0)], vec![]).unwrap();
2406+
}
2407+
2408+
let mut reload_persister = create_persister.clone();
2409+
let wallet = load_empty_wallet_with_lookahead(&mut reload_persister, Some(3));
2410+
let mut aggregate = AggregateWallet::<u8, _>::new(wallet, reload_persister, 0, vec![]);
2411+
assert_eq!(aggregate.new_address_info_for(&0).unwrap().index, 3);
2412+
}
2413+
22862414
#[test]
22872415
fn address_info_for_index_supports_internal_keychain_without_advancing_receive_index() {
22882416
let mut persister = NoopPersister;

src/builder.rs

Lines changed: 70 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ use crate::config::{
5252
default_user_config, may_announce_channel, AddressType, AddressTypeRuntimeConfig,
5353
AnnounceError, AsyncPaymentsRole, BitcoindRestClientConfig, Config, ElectrumSyncConfig,
5454
EsploraSyncConfig, OnchainWalletAccount, RuntimeSyncIntervals, ScoringDecayParameters,
55-
ScoringFeeParameters, DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL,
56-
MAX_ONCHAIN_WALLET_ACCOUNT_INDEX, WALLET_KEYS_SEED_LEN,
55+
ScoringFeeParameters, BDK_CLIENT_STOP_GAP, DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME,
56+
DEFAULT_LOG_LEVEL, MAX_ONCHAIN_WALLET_ACCOUNT_INDEX, WALLET_KEYS_SEED_LEN,
5757
};
5858
use crate::connection::ConnectionManager;
5959
use crate::event::EventQueue;
@@ -115,6 +115,18 @@ enum ChainDataSourceConfig {
115115
},
116116
}
117117

118+
fn derived_account_lookahead(chain_data_source_config: Option<&ChainDataSourceConfig>) -> u32 {
119+
match chain_data_source_config {
120+
Some(ChainDataSourceConfig::Electrum { sync_config, .. }) => {
121+
sync_config.unwrap_or_default().additional_wallet_full_scan_stop_gap.max(1)
122+
},
123+
Some(ChainDataSourceConfig::Bitcoind { .. }) => {
124+
bdk_chain::keychain_txout::DEFAULT_LOOKAHEAD
125+
},
126+
Some(ChainDataSourceConfig::Esplora { .. }) | None => BDK_CLIENT_STOP_GAP as u32,
127+
}
128+
}
129+
118130
#[derive(Debug, Clone)]
119131
enum EntropySourceConfig {
120132
SeedFile(String),
@@ -1350,42 +1362,49 @@ fn account_keychain_derivation_path(
13501362
/// persisted wallet was restored.
13511363
pub(crate) fn get_or_create_wallet_for_account(
13521364
wallet_account: OnchainWalletAccount, master_xprv: Xpriv, network: Network,
1353-
persister: &mut KVStoreWalletPersister,
1365+
persister: &mut KVStoreWalletPersister, lookahead: Option<u32>,
13541366
) -> Result<(PersistedWallet<KVStoreWalletPersister>, bool), BuildError> {
13551367
macro_rules! load_or_create {
13561368
($ext:expr, $int:expr) => {{
1357-
let wallet_opt = BdkWallet::load()
1369+
let mut load_params = BdkWallet::load()
13581370
.descriptor(KeychainKind::External, Some($ext))
13591371
.descriptor(KeychainKind::Internal, Some($int))
13601372
.extract_keys()
1361-
.check_network(network)
1362-
.load_wallet(persister)
1363-
.map_err(|e| match e {
1364-
bdk_wallet::LoadWithPersistError::InvalidChangeSet(
1365-
bdk_wallet::LoadError::Mismatch(bdk_wallet::LoadMismatch::Network {
1366-
loaded,
1367-
expected,
1368-
}),
1369-
) => {
1370-
log::error!(
1371-
"Failed to setup wallet: Networks do not match. Expected {} but got {}",
1372-
expected,
1373-
loaded
1374-
);
1375-
BuildError::NetworkMismatch
1376-
},
1377-
_ => {
1378-
log::error!("Failed to set up wallet: {}", e);
1379-
BuildError::WalletSetupFailed
1380-
},
1381-
})?;
1373+
.check_network(network);
1374+
if let Some(lookahead) = lookahead {
1375+
load_params = load_params.lookahead(lookahead);
1376+
}
1377+
let wallet_opt = load_params.load_wallet(persister).map_err(|e| match e {
1378+
bdk_wallet::LoadWithPersistError::InvalidChangeSet(
1379+
bdk_wallet::LoadError::Mismatch(bdk_wallet::LoadMismatch::Network {
1380+
loaded,
1381+
expected,
1382+
}),
1383+
) => {
1384+
log::error!(
1385+
"Failed to setup wallet: Networks do not match. Expected {} but got {}",
1386+
expected,
1387+
loaded
1388+
);
1389+
BuildError::NetworkMismatch
1390+
},
1391+
_ => {
1392+
log::error!("Failed to set up wallet: {}", e);
1393+
BuildError::WalletSetupFailed
1394+
},
1395+
})?;
13821396
match wallet_opt {
13831397
Some(w) => Ok((w, true)),
1384-
None => BdkWallet::create($ext, $int)
1385-
.network(network)
1386-
.create_wallet(persister)
1387-
.map(|w| (w, false))
1388-
.map_err(|_| BuildError::WalletSetupFailed),
1398+
None => {
1399+
let mut create_params = BdkWallet::create($ext, $int).network(network);
1400+
if let Some(lookahead) = lookahead {
1401+
create_params = create_params.lookahead(lookahead);
1402+
}
1403+
create_params
1404+
.create_wallet(persister)
1405+
.map(|w| (w, false))
1406+
.map_err(|_| BuildError::WalletSetupFailed)
1407+
},
13891408
}
13901409
}};
13911410
}
@@ -1484,20 +1503,25 @@ fn validate_configured_onchain_wallet_accounts(
14841503

14851504
fn build_wallet_for_account(
14861505
config: &Config, wallet_account: OnchainWalletAccount, xprv: Xpriv, kv_store: Arc<DynStore>,
1487-
logger: Arc<Logger>, chain_tip_opt: Option<&BestBlock>,
1506+
logger: Arc<Logger>, chain_tip_opt: Option<&BestBlock>, lookahead: Option<u32>,
14881507
) -> Result<
14891508
(OnchainWalletAccount, PersistedWallet<KVStoreWalletPersister>, KVStoreWalletPersister),
14901509
BuildError,
14911510
> {
14921511
let mut persister =
14931512
KVStoreWalletPersister::new(Arc::clone(&kv_store), Arc::clone(&logger), wallet_account);
14941513

1495-
let (mut wallet, loaded_from_store) =
1496-
get_or_create_wallet_for_account(wallet_account, xprv, config.network, &mut persister)
1497-
.map_err(|e| {
1498-
log_error!(logger, "Failed to setup wallet for {:?}: {}", wallet_account, e);
1499-
e
1500-
})?;
1514+
let (mut wallet, loaded_from_store) = get_or_create_wallet_for_account(
1515+
wallet_account,
1516+
xprv,
1517+
config.network,
1518+
&mut persister,
1519+
lookahead,
1520+
)
1521+
.map_err(|e| {
1522+
log_error!(logger, "Failed to setup wallet for {:?}: {}", wallet_account, e);
1523+
e
1524+
})?;
15011525

15021526
// Re-loaded wallets keep their persisted tip so chain backends can catch up. New wallets start
15031527
// at the known tip; Esplora and Electrum recover history independently with a full scan, while
@@ -1522,6 +1546,7 @@ fn build_wallet_for_account(
15221546
fn build_additional_wallets(
15231547
config: &Config, configured_accounts: &[OnchainWalletAccount], xprv: Xpriv,
15241548
kv_store: Arc<DynStore>, logger: Arc<Logger>, chain_tip_opt: Option<&BestBlock>,
1549+
derived_account_lookahead: u32,
15251550
) -> Result<
15261551
Vec<(OnchainWalletAccount, PersistedWallet<KVStoreWalletPersister>, KVStoreWalletPersister)>,
15271552
BuildError,
@@ -1537,6 +1562,7 @@ fn build_additional_wallets(
15371562
Arc::clone(&kv_store),
15381563
Arc::clone(&logger),
15391564
chain_tip_opt,
1565+
None,
15401566
) {
15411567
Ok(tuple) => {
15421568
log_info!(logger, "Created additional wallet for {:?}", tuple.0);
@@ -1561,6 +1587,7 @@ fn build_additional_wallets(
15611587
Arc::clone(&kv_store),
15621588
Arc::clone(&logger),
15631589
chain_tip_opt,
1590+
Some(derived_account_lookahead),
15641591
)?;
15651592
log_info!(logger, "Loaded configured wallet for {:?}", wallet_account);
15661593
additional_wallets.push(wallet);
@@ -1755,6 +1782,7 @@ fn build_with_store_internal(
17551782
BuildError::InvalidSeedBytes
17561783
})?;
17571784
let configured_accounts = validate_configured_onchain_wallet_accounts(&config, xprv)?;
1785+
let derived_account_lookahead = derived_account_lookahead(chain_data_source_config);
17581786

17591787
let tx_broadcaster = Arc::new(TransactionBroadcaster::new(Arc::clone(&logger)));
17601788
let fee_estimator = Arc::new(OnchainFeeEstimator::new());
@@ -1785,6 +1813,7 @@ fn build_with_store_internal(
17851813
xprv,
17861814
network,
17871815
&mut persister,
1816+
None,
17881817
)
17891818
.map(|(wallet, _loaded)| wallet);
17901819
(result, persister)
@@ -1951,6 +1980,7 @@ fn build_with_store_internal(
19511980
Arc::clone(&kv_store),
19521981
Arc::clone(&logger),
19531982
chain_tip_opt.as_ref(),
1983+
derived_account_lookahead,
19541984
)?;
19551985

19561986
let wallet = Arc::new(Wallet::new(
@@ -1966,6 +1996,7 @@ fn build_with_store_internal(
19661996
Arc::clone(&address_type_runtime_config),
19671997
Arc::clone(&node_metrics),
19681998
Arc::clone(&logger),
1999+
derived_account_lookahead,
19692000
));
19702001

19712002
// Initialize the KeysManager
@@ -3291,7 +3322,8 @@ mod tests {
32913322
let mut persister =
32923323
KVStoreWalletPersister::new(Arc::clone(&store), Arc::clone(&logger), account);
32933324
let (wallet, loaded) =
3294-
get_or_create_wallet_for_account(account, master, network, &mut persister).unwrap();
3325+
get_or_create_wallet_for_account(account, master, network, &mut persister, None)
3326+
.unwrap();
32953327
assert!(!loaded);
32963328

32973329
let descriptor = wallet.public_descriptor(KeychainKind::External).to_string();

0 commit comments

Comments
 (0)