Skip to content

Commit f128c2c

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 4a6eb16 commit f128c2c

File tree

4 files changed

+62
-16
lines changed

4 files changed

+62
-16
lines changed

bindings/ldk_node.udl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ interface Builder {
129129
void set_node_alias(string node_alias);
130130
[Throws=BuildError]
131131
void set_async_payments_role(AsyncPaymentsRole? role);
132+
void set_wallet_recovery_mode();
132133
[Throws=BuildError]
133134
Node build(NodeEntropy node_entropy);
134135
[Throws=BuildError]

src/builder.rs

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ pub struct NodeBuilder {
244244
async_payments_role: Option<AsyncPaymentsRole>,
245245
runtime_handle: Option<tokio::runtime::Handle>,
246246
pathfinding_scores_sync_config: Option<PathfindingScoresSyncConfig>,
247+
recovery_mode: bool,
247248
}
248249

249250
impl NodeBuilder {
@@ -261,6 +262,7 @@ impl NodeBuilder {
261262
let log_writer_config = None;
262263
let runtime_handle = None;
263264
let pathfinding_scores_sync_config = None;
265+
let recovery_mode = false;
264266
Self {
265267
config,
266268
chain_data_source_config,
@@ -270,6 +272,7 @@ impl NodeBuilder {
270272
runtime_handle,
271273
async_payments_role: None,
272274
pathfinding_scores_sync_config,
275+
recovery_mode,
273276
}
274277
}
275278

@@ -544,6 +547,16 @@ impl NodeBuilder {
544547
Ok(self)
545548
}
546549

550+
/// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any
551+
/// historical wallet funds.
552+
///
553+
/// This should only be set on first startup when importing an older wallet from a previously
554+
/// used [`NodeEntropy`].
555+
pub fn set_wallet_recovery_mode(&mut self) -> &mut Self {
556+
self.recovery_mode = true;
557+
self
558+
}
559+
547560
/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
548561
/// previously configured.
549562
pub fn build(&self, node_entropy: NodeEntropy) -> Result<Node, BuildError> {
@@ -679,6 +692,7 @@ impl NodeBuilder {
679692
self.liquidity_source_config.as_ref(),
680693
self.pathfinding_scores_sync_config.as_ref(),
681694
self.async_payments_role,
695+
self.recovery_mode,
682696
seed_bytes,
683697
runtime,
684698
logger,
@@ -919,6 +933,15 @@ impl ArcedNodeBuilder {
919933
self.inner.write().unwrap().set_async_payments_role(role).map(|_| ())
920934
}
921935

936+
/// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any
937+
/// historical wallet funds.
938+
///
939+
/// This should only be set on first startup when importing an older wallet from a previously
940+
/// used [`NodeEntropy`].
941+
pub fn set_wallet_recovery_mode(&self) {
942+
self.inner.write().unwrap().set_wallet_recovery_mode();
943+
}
944+
922945
/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
923946
/// previously configured.
924947
pub fn build(&self, node_entropy: Arc<NodeEntropy>) -> Result<Arc<Node>, BuildError> {
@@ -1033,8 +1056,8 @@ fn build_with_store_internal(
10331056
gossip_source_config: Option<&GossipSourceConfig>,
10341057
liquidity_source_config: Option<&LiquiditySourceConfig>,
10351058
pathfinding_scores_sync_config: Option<&PathfindingScoresSyncConfig>,
1036-
async_payments_role: Option<AsyncPaymentsRole>, seed_bytes: [u8; 64], runtime: Arc<Runtime>,
1037-
logger: Arc<Logger>, kv_store: Arc<DynStore>,
1059+
async_payments_role: Option<AsyncPaymentsRole>, recovery_mode: bool, seed_bytes: [u8; 64],
1060+
runtime: Arc<Runtime>, logger: Arc<Logger>, kv_store: Arc<DynStore>,
10381061
) -> Result<Node, BuildError> {
10391062
optionally_install_rustls_cryptoprovider();
10401063

@@ -1230,19 +1253,23 @@ fn build_with_store_internal(
12301253
BuildError::WalletSetupFailed
12311254
})?;
12321255

1233-
if let Some(best_block) = chain_tip_opt {
1234-
// Insert the first checkpoint if we have it, to avoid resyncing from genesis.
1235-
// TODO: Use a proper wallet birthday once BDK supports it.
1236-
let mut latest_checkpoint = wallet.latest_checkpoint();
1237-
let block_id =
1238-
bdk_chain::BlockId { height: best_block.height, hash: best_block.block_hash };
1239-
latest_checkpoint = latest_checkpoint.insert(block_id);
1240-
let update =
1241-
bdk_wallet::Update { chain: Some(latest_checkpoint), ..Default::default() };
1242-
wallet.apply_update(update).map_err(|e| {
1243-
log_error!(logger, "Failed to apply checkpoint during wallet setup: {}", e);
1244-
BuildError::WalletSetupFailed
1245-
})?;
1256+
if !recovery_mode {
1257+
if let Some(best_block) = chain_tip_opt {
1258+
// Insert the first checkpoint if we have it, to avoid resyncing from genesis.
1259+
// TODO: Use a proper wallet birthday once BDK supports it.
1260+
let mut latest_checkpoint = wallet.latest_checkpoint();
1261+
let block_id = bdk_chain::BlockId {
1262+
height: best_block.height,
1263+
hash: best_block.block_hash,
1264+
};
1265+
latest_checkpoint = latest_checkpoint.insert(block_id);
1266+
let update =
1267+
bdk_wallet::Update { chain: Some(latest_checkpoint), ..Default::default() };
1268+
wallet.apply_update(update).map_err(|e| {
1269+
log_error!(logger, "Failed to apply checkpoint during wallet setup: {}", e);
1270+
BuildError::WalletSetupFailed
1271+
})?;
1272+
}
12461273
}
12471274
wallet
12481275
},

tests/common/mod.rs

Lines changed: 18 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

@@ -441,6 +450,10 @@ pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) ->
441450

442451
builder.set_async_payments_role(config.async_payments_role).unwrap();
443452

453+
if config.recovery_mode {
454+
builder.set_wallet_recovery_mode();
455+
}
456+
444457
let node = match config.store_type {
445458
TestStoreType::TestSyncStore => {
446459
let kv_store = TestSyncStore::new(config.node_config.storage_dir_path.into());
@@ -449,6 +462,10 @@ pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) ->
449462
TestStoreType::Sqlite => builder.build(config.node_entropy.into()).unwrap(),
450463
};
451464

465+
if config.recovery_mode {
466+
builder.set_wallet_recovery_mode();
467+
}
468+
452469
node.start().unwrap();
453470
assert!(node.status().is_running);
454471
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
@@ -626,6 +626,7 @@ async fn onchain_wallet_recovery() {
626626
// Now we start from scratch, only the seed remains the same.
627627
let mut recovered_config = random_config(true);
628628
recovered_config.node_entropy = original_node_entropy;
629+
recovered_config.recovery_mode = true;
629630
let recovered_node = setup_node(&chain_source, recovered_config);
630631

631632
recovered_node.sync_wallets().unwrap();

0 commit comments

Comments
 (0)