Skip to content

Commit 9becbbd

Browse files
ben-kaufmanclaude
andcommitted
feat: accept stale channel monitors for recovery from monitor desync
When a channel monitor's update_id falls behind the ChannelManager (e.g. after a migration overwrote newer data with stale backup data), LDK refuses to start with DangerousValue. This adds a recovery path: - Builder: new `set_accept_stale_channel_monitors(bool)` flag - Build: passes flag to ChannelManagerReadArgs, which force-syncs stale monitor update_ids instead of returning DangerousValue - Startup: when flag is set, defers chain sync while sending probes on all channels to trigger commitment round-trips that heal the stale monitor state. Polls monitor update_ids with 60s timeout and retries probes every 10s for late-connecting peers. The probe-triggered commitment round-trip provides: - LatestHolderCommitmentTXInfo (correct current commitment state) - CommitmentSecret (recovers all gap revocation secrets via the derivation tree) Depends on: ben-kaufman/rust-lightning#fix/accept-stale-channel-monitors Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f045195 commit 9becbbd

4 files changed

Lines changed: 263 additions & 32 deletions

File tree

Cargo.toml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -124,18 +124,18 @@ check-cfg = [
124124
name = "payments"
125125
harness = false
126126

127-
#[patch.crates-io]
128-
#lightning = { path = "../rust-lightning/lightning" }
129-
#lightning-types = { path = "../rust-lightning/lightning-types" }
130-
#lightning-invoice = { path = "../rust-lightning/lightning-invoice" }
131-
#lightning-net-tokio = { path = "../rust-lightning/lightning-net-tokio" }
132-
#lightning-persister = { path = "../rust-lightning/lightning-persister" }
133-
#lightning-background-processor = { path = "../rust-lightning/lightning-background-processor" }
134-
#lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" }
135-
#lightning-block-sync = { path = "../rust-lightning/lightning-block-sync" }
136-
#lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync" }
137-
#lightning-liquidity = { path = "../rust-lightning/lightning-liquidity" }
138-
#lightning-macros = { path = "../rust-lightning/lightning-macros" }
127+
[patch.crates-io]
128+
lightning = { git = "https://github.com/ben-kaufman/rust-lightning", branch = "fix/accept-stale-channel-monitors" }
129+
lightning-types = { git = "https://github.com/ben-kaufman/rust-lightning", branch = "fix/accept-stale-channel-monitors" }
130+
lightning-invoice = { git = "https://github.com/ben-kaufman/rust-lightning", branch = "fix/accept-stale-channel-monitors" }
131+
lightning-net-tokio = { git = "https://github.com/ben-kaufman/rust-lightning", branch = "fix/accept-stale-channel-monitors" }
132+
lightning-persister = { git = "https://github.com/ben-kaufman/rust-lightning", branch = "fix/accept-stale-channel-monitors" }
133+
lightning-background-processor = { git = "https://github.com/ben-kaufman/rust-lightning", branch = "fix/accept-stale-channel-monitors" }
134+
lightning-rapid-gossip-sync = { git = "https://github.com/ben-kaufman/rust-lightning", branch = "fix/accept-stale-channel-monitors" }
135+
lightning-block-sync = { git = "https://github.com/ben-kaufman/rust-lightning", branch = "fix/accept-stale-channel-monitors" }
136+
lightning-transaction-sync = { git = "https://github.com/ben-kaufman/rust-lightning", branch = "fix/accept-stale-channel-monitors" }
137+
lightning-liquidity = { git = "https://github.com/ben-kaufman/rust-lightning", branch = "fix/accept-stale-channel-monitors" }
138+
lightning-macros = { git = "https://github.com/ben-kaufman/rust-lightning", branch = "fix/accept-stale-channel-monitors" }
139139

140140
#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" }
141141
#lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" }

bindings/ldk_node.udl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ interface Builder {
109109
void set_entropy_seed_bytes(sequence<u8> seed_bytes);
110110
void set_entropy_bip39_mnemonic(Mnemonic mnemonic, string? passphrase);
111111
void set_channel_data_migration(ChannelDataMigration migration);
112+
void set_accept_stale_channel_monitors(boolean accept);
112113
void set_chain_source_esplora(string server_url, EsploraSyncConfig? config);
113114
void set_chain_source_electrum(string server_url, ElectrumSyncConfig? config);
114115
void set_chain_source_bitcoind_rpc(string rpc_host, u16 rpc_port, string rpc_user, string rpc_password);

src/builder.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ pub struct NodeBuilder {
279279
runtime_handle: Option<tokio::runtime::Handle>,
280280
pathfinding_scores_sync_config: Option<PathfindingScoresSyncConfig>,
281281
channel_data_migration: Option<ChannelDataMigration>,
282+
accept_stale_channel_monitors: bool,
282283
}
283284

284285
impl NodeBuilder {
@@ -309,6 +310,7 @@ impl NodeBuilder {
309310
async_payments_role: None,
310311
pathfinding_scores_sync_config,
311312
channel_data_migration,
313+
accept_stale_channel_monitors: false,
312314
}
313315
}
314316

@@ -361,6 +363,19 @@ impl NodeBuilder {
361363
self
362364
}
363365

366+
/// Accept stale channel monitors on startup instead of failing with `DangerousValue`.
367+
///
368+
/// When enabled, stale monitors have their `update_id` force-synced to match the
369+
/// `ChannelManager`. The monitor's commitment state remains stale until the next real
370+
/// channel update (e.g. a fee update round-trip after reconnecting to the peer).
371+
///
372+
/// Use this for recovery after monitor data was overwritten by a migration or backup restore.
373+
/// Chain sync should be delayed until monitors are healed via a commitment round-trip.
374+
pub fn set_accept_stale_channel_monitors(&mut self, accept: bool) -> &mut Self {
375+
self.accept_stale_channel_monitors = accept;
376+
self
377+
}
378+
364379
/// Configures the [`Node`] instance to source its chain data from the given Esplora server.
365380
///
366381
/// If no `sync_config` is given, default values are used. See [`EsploraSyncConfig`] for more
@@ -813,6 +828,7 @@ impl NodeBuilder {
813828
logger,
814829
Arc::new(vss_store),
815830
self.channel_data_migration.as_ref(),
831+
self.accept_stale_channel_monitors,
816832
)
817833
}
818834

@@ -848,6 +864,7 @@ impl NodeBuilder {
848864
logger,
849865
kv_store,
850866
self.channel_data_migration.as_ref(),
867+
self.accept_stale_channel_monitors,
851868
)
852869
}
853870
}
@@ -917,6 +934,13 @@ impl ArcedNodeBuilder {
917934
self.inner.write().unwrap().set_channel_data_migration(migration);
918935
}
919936

937+
/// Accept stale channel monitors on startup instead of failing.
938+
///
939+
/// See [`NodeBuilder::set_accept_stale_channel_monitors`] for details.
940+
pub fn set_accept_stale_channel_monitors(&self, accept: bool) {
941+
self.inner.write().unwrap().set_accept_stale_channel_monitors(accept);
942+
}
943+
920944
/// Configures the [`Node`] instance to source its chain data from the given Esplora server.
921945
///
922946
/// If no `sync_config` is given, default values are used. See [`EsploraSyncConfig`] for more
@@ -1346,7 +1370,7 @@ fn build_with_store_internal(
13461370
pathfinding_scores_sync_config: Option<&PathfindingScoresSyncConfig>,
13471371
async_payments_role: Option<AsyncPaymentsRole>, seed_bytes: [u8; 64], runtime: Arc<Runtime>,
13481372
logger: Arc<Logger>, kv_store: Arc<DynStore>,
1349-
channel_data_migration: Option<&ChannelDataMigration>,
1373+
channel_data_migration: Option<&ChannelDataMigration>, accept_stale_channel_monitors: bool,
13501374
) -> Result<Node, BuildError> {
13511375
optionally_install_rustls_cryptoprovider();
13521376

@@ -1876,7 +1900,7 @@ fn build_with_store_internal(
18761900
let mut reader = Cursor::new(res);
18771901
let channel_monitor_references =
18781902
channel_monitors.iter().map(|(_, chanmon)| chanmon).collect();
1879-
let read_args = ChannelManagerReadArgs::new(
1903+
let mut read_args = ChannelManagerReadArgs::new(
18801904
Arc::clone(&keys_manager),
18811905
Arc::clone(&keys_manager),
18821906
Arc::clone(&keys_manager),
@@ -1889,6 +1913,7 @@ fn build_with_store_internal(
18891913
user_config,
18901914
channel_monitor_references,
18911915
);
1916+
read_args.accept_stale_channel_monitors = accept_stale_channel_monitors;
18921917
let (_hash, channel_manager) =
18931918
<(BlockHash, ChannelManager)>::read(&mut reader, read_args).map_err(|e| {
18941919
log_error!(logger, "Failed to read channel manager from store: {}", e);
@@ -2250,6 +2275,7 @@ fn build_with_store_internal(
22502275
async_payments_role,
22512276
runtime_sync_intervals: Arc::new(RwLock::new(RuntimeSyncIntervals::default())),
22522277
local_rgs_timestamp,
2278+
accept_stale_channel_monitors,
22532279
})
22542280
}
22552281

0 commit comments

Comments
 (0)