Skip to content

Commit 96b6f29

Browse files
committed
Fix: Add (onchain) recovery mode
Previously, we fixed than a fresh node syncing via `bitcoind` RPC would resync all chain data back to genesis. However, while introducing a wallet birthday is great, it disallowed discovery of historical funds if a wallet would be imported from seed. Here, we add a recovery mode flag to the builder that explictly allows to re-enable resyncing from genesis in such a scenario. Going forward, we intend to reuse that API for an upcoming Lightning recoery flow, too.
1 parent ef6895d commit 96b6f29

File tree

4 files changed

+58
-16
lines changed

4 files changed

+58
-16
lines changed

bindings/ldk_node.udl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ interface Builder {
119119
void set_node_alias(string node_alias);
120120
[Throws=BuildError]
121121
void set_async_payments_role(AsyncPaymentsRole? role);
122+
void set_wallet_recovery_mode();
122123
[Throws=BuildError]
123124
Node build(NodeEntropy node_entropy);
124125
[Throws=BuildError]

src/builder.rs

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ pub struct NodeBuilder {
241241
async_payments_role: Option<AsyncPaymentsRole>,
242242
runtime_handle: Option<tokio::runtime::Handle>,
243243
pathfinding_scores_sync_config: Option<PathfindingScoresSyncConfig>,
244+
recovery_mode: bool,
244245
}
245246

246247
impl NodeBuilder {
@@ -258,6 +259,7 @@ impl NodeBuilder {
258259
let log_writer_config = None;
259260
let runtime_handle = None;
260261
let pathfinding_scores_sync_config = None;
262+
let recovery_mode = false;
261263
Self {
262264
config,
263265
chain_data_source_config,
@@ -267,6 +269,7 @@ impl NodeBuilder {
267269
runtime_handle,
268270
async_payments_role: None,
269271
pathfinding_scores_sync_config,
272+
recovery_mode,
270273
}
271274
}
272275

@@ -541,6 +544,16 @@ impl NodeBuilder {
541544
Ok(self)
542545
}
543546

547+
/// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any
548+
/// historical wallet funds.
549+
///
550+
/// This should only be set on first startup when importing an older wallet from a previously
551+
/// used [`NodeEntropy`].
552+
pub fn set_wallet_recovery_mode(&mut self) -> &mut Self {
553+
self.recovery_mode = true;
554+
self
555+
}
556+
544557
/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
545558
/// previously configured.
546559
pub fn build(&self, node_entropy: NodeEntropy) -> Result<Node, BuildError> {
@@ -676,6 +689,7 @@ impl NodeBuilder {
676689
self.liquidity_source_config.as_ref(),
677690
self.pathfinding_scores_sync_config.as_ref(),
678691
self.async_payments_role,
692+
self.recovery_mode,
679693
seed_bytes,
680694
runtime,
681695
logger,
@@ -916,6 +930,15 @@ impl ArcedNodeBuilder {
916930
self.inner.write().unwrap().set_async_payments_role(role).map(|_| ())
917931
}
918932

933+
/// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any
934+
/// historical wallet funds.
935+
///
936+
/// This should only be set on first startup when importing an older wallet from a previously
937+
/// used [`NodeEntropy`].
938+
pub fn set_wallet_recovery_mode(&self) {
939+
self.inner.write().unwrap().set_wallet_recovery_mode();
940+
}
941+
919942
/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
920943
/// previously configured.
921944
pub fn build(&self, node_entropy: Arc<NodeEntropy>) -> Result<Arc<Node>, BuildError> {
@@ -1030,8 +1053,8 @@ fn build_with_store_internal(
10301053
gossip_source_config: Option<&GossipSourceConfig>,
10311054
liquidity_source_config: Option<&LiquiditySourceConfig>,
10321055
pathfinding_scores_sync_config: Option<&PathfindingScoresSyncConfig>,
1033-
async_payments_role: Option<AsyncPaymentsRole>, seed_bytes: [u8; 64], runtime: Arc<Runtime>,
1034-
logger: Arc<Logger>, kv_store: Arc<DynStore>,
1056+
async_payments_role: Option<AsyncPaymentsRole>, recovery_mode: bool, seed_bytes: [u8; 64],
1057+
runtime: Arc<Runtime>, logger: Arc<Logger>, kv_store: Arc<DynStore>,
10351058
) -> Result<Node, BuildError> {
10361059
optionally_install_rustls_cryptoprovider();
10371060

@@ -1225,19 +1248,23 @@ fn build_with_store_internal(
12251248
BuildError::WalletSetupFailed
12261249
})?;
12271250

1228-
if let Some(best_block) = chain_tip_opt {
1229-
// Insert the first checkpoint if we have it, to avoid resyncing from genesis.
1230-
// TODO: Use a proper wallet birthday once BDK supports it.
1231-
let mut latest_checkpoint = wallet.latest_checkpoint();
1232-
let block_id =
1233-
bdk_chain::BlockId { height: best_block.height, hash: best_block.block_hash };
1234-
latest_checkpoint = latest_checkpoint.insert(block_id);
1235-
let update =
1236-
bdk_wallet::Update { chain: Some(latest_checkpoint), ..Default::default() };
1237-
wallet.apply_update(update).map_err(|e| {
1238-
log_error!(logger, "Failed to apply checkpoint during wallet setup: {}", e);
1239-
BuildError::WalletSetupFailed
1240-
})?;
1251+
if !recovery_mode {
1252+
if let Some(best_block) = chain_tip_opt {
1253+
// Insert the first checkpoint if we have it, to avoid resyncing from genesis.
1254+
// TODO: Use a proper wallet birthday once BDK supports it.
1255+
let mut latest_checkpoint = wallet.latest_checkpoint();
1256+
let block_id = bdk_chain::BlockId {
1257+
height: best_block.height,
1258+
hash: best_block.block_hash,
1259+
};
1260+
latest_checkpoint = latest_checkpoint.insert(block_id);
1261+
let update =
1262+
bdk_wallet::Update { chain: Some(latest_checkpoint), ..Default::default() };
1263+
wallet.apply_update(update).map_err(|e| {
1264+
log_error!(logger, "Failed to apply checkpoint during wallet setup: {}", e);
1265+
BuildError::WalletSetupFailed
1266+
})?;
1267+
}
12411268
}
12421269
wallet
12431270
},

tests/common/mod.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ pub(crate) struct TestConfig {
318318
pub store_type: TestStoreType,
319319
pub node_entropy: NodeEntropy,
320320
pub async_payments_role: Option<AsyncPaymentsRole>,
321+
pub recovery_mode: bool,
321322
}
322323

323324
impl Default for TestConfig {
@@ -329,7 +330,15 @@ impl Default for TestConfig {
329330
let mnemonic = generate_entropy_mnemonic(None);
330331
let node_entropy = NodeEntropy::from_bip39_mnemonic(mnemonic, None);
331332
let async_payments_role = None;
332-
TestConfig { node_config, log_writer, store_type, node_entropy, async_payments_role }
333+
let recovery_mode = false;
334+
TestConfig {
335+
node_config,
336+
log_writer,
337+
store_type,
338+
node_entropy,
339+
async_payments_role,
340+
recovery_mode,
341+
}
333342
}
334343
}
335344

@@ -447,6 +456,10 @@ pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) ->
447456
TestStoreType::Sqlite => builder.build(config.node_entropy.into()).unwrap(),
448457
};
449458

459+
if config.recovery_mode {
460+
builder.set_wallet_recovery_mode();
461+
}
462+
450463
node.start().unwrap();
451464
assert!(node.status().is_running);
452465
assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some());

tests/integration_tests_rust.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,7 @@ async fn onchain_wallet_recovery() {
624624
// Now we start from scratch, only the seed remains the same.
625625
let mut recovered_config = random_config(true);
626626
recovered_config.node_entropy = original_node_entropy;
627+
recovered_config.recovery_mode = true;
627628
let recovered_node = setup_node(&chain_source, recovered_config);
628629

629630
recovered_node.sync_wallets().unwrap();

0 commit comments

Comments
 (0)