Skip to content

fix: recover orphaned channel monitors from RN migration#462

Open
jvsena42 wants to merge 8 commits intomasterfrom
fix/reimport-channel-monitor
Open

fix: recover orphaned channel monitors from RN migration#462
jvsena42 wants to merge 8 commits intomasterfrom
fix/reimport-channel-monitor

Conversation

@jvsena42
Copy link
Member

@jvsena42 jvsena42 commented Feb 25, 2026

Fixes #459

This PR adds a one-time post-startup recovery check for orphaned channel monitors from the RN migration. During migration, some channel monitors could silently fail to download from the RN backup server, leaving LDK unable to sweep force-closed channel funds. This recovery mechanism re-fetches those monitors and restarts the node with the recovered data.

Ported from the Android implementation on fix/reimport-channel-monitor.

How it works

  1. After the Lightning node starts, checkForOrphanedChannelMonitorRecovery() runs once
  2. Sets up RNBackupClient and calls fetchRNRemoteLdkData() to retrieve monitors from the RN backup
  3. If monitors are found, stops the node, waits 500ms, and restarts with the recovered channel migration data
  4. Only marks the check as complete if all monitors downloaded successfully — otherwise retries on next startup

Changes

  • MigrationsService: add isChannelRecoveryChecked flag, make fetchRNRemoteLdkData() public and return Bool indicating whether all monitors were retrieved
  • WalletViewModel: add checkForOrphanedChannelMonitorRecovery(), called after node start

QA Notes

  1. Fresh install from a migrated-from-RN wallet
    • Setup Lightning channels on RN wallet
    • Backup the seed-phrase and delete the app
    • recover on native with this code to simulate a failure

  func retrieveChannelMonitor(channelId: String) async throws -> Data {
      throw RNBackupError.invalidServerResponse("Retrieve channel_monitor \(channelId) failed") //TODO DON'T COMMIT

      let bearer = try await getAuthToken()
      let url = try buildUrl(method: "retrieve", label: "channel_monitor", fileGroup: "ldk", channelId: channelId)

      var request = URLRequest(url: url)
      request.httpMethod = "GET"
      request.setValue("application/json", forHTTPHeaderField: "Content-Type")
      request.setValue(bearer, forHTTPHeaderField: "Authorization")

      let (data, response) = try await URLSession.shared.data(for: request)

      guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
          throw RNBackupError.invalidServerResponse("Retrieve channel_monitor \(channelId) failed")
      }

      return try decrypt(data)
  }
  • LDN node will send a bogus message to counterparty to force-close the channel
  • mine 146 blocks to confirm the force-close transaction
  • run the code again without the throw RNBackupError.invalidServerResponse("Retrieve channel_monitor \(channelId) failed") //TODO DON'T COMMIT
  • Expected: The wallet will claim the force close funds
failure-simulation.mp4
channel-closing-detection.mp4

@jvsena42 jvsena42 self-assigned this Feb 25, 2026
@jvsena42
Copy link
Member Author

Failure simulation logs:

DEBUG: Retrying channel monitor retrieval for fa07160b53153ef1f91bb8f538293c5d43b534460441b3b057cd7f5f6a029837 (attempt 2/3) after 1000ms: invalidServerResponse("Retrieve channel_monitor fa07160b53153ef1f91bb8f538293c5d43b534460441b3b057cd7f5f6a029837 failed") - Migration [MigrationsService.swift: retrieveChannelMonitorWithRetry(channelId:maxAttempts:baseDelayMs:) line: 1955]
DEBUG: Retrying channel monitor retrieval for 84f85b5cc7d8b2e6ae3b565c9f6441339047658c76cb9b7a0931dc338085f3cd (attempt 2/3) after 1000ms: invalidServerResponse("Retrieve channel_monitor 84f85b5cc7d8b2e6ae3b565c9f6441339047658c76cb9b7a0931dc338085f3cd failed") - Migration [MigrationsService.swift: retrieveChannelMonitorWithRetry(channelId:maxAttempts:baseDelayMs:) line: 1955]
DEBUG: Retrying channel monitor retrieval for 84f85b5cc7d8b2e6ae3b565c9f6441339047658c76cb9b7a0931dc338085f3cd (attempt 3/3) after 2000ms: invalidServerResponse("Retrieve channel_monitor 84f85b5cc7d8b2e6ae3b565c9f6441339047658c76cb9b7a0931dc338085f3cd failed") - Migration [MigrationsService.swift: retrieveChannelMonitorWithRetry(channelId:maxAttempts:baseDelayMs:) line: 1955]
DEBUG: Retrying channel monitor retrieval for fa07160b53153ef1f91bb8f538293c5d43b534460441b3b057cd7f5f6a029837 (attempt 3/3) after 2000ms: invalidServerResponse("Retrieve channel_monitor fa07160b53153ef1f91bb8f538293c5d43b534460441b3b057cd7f5f6a029837 failed") - Migration [MigrationsService.swift: retrieveChannelMonitorWithRetry(channelId:maxAttempts:baseDelayMs:) line: 1955]
ERROR❌: Failed to retrieve 2/2 channel monitors after retries: 84f85b5cc7d8b2e6ae3b565c9f6441339047658c76cb9b7a0931dc338085f3cd, 

After mining 146 blocks and run the fix:

INFOℹ️: Running one-time channel monitor recovery check - WalletViewModel [WalletViewModel.swift: checkForOrphanedChannelMonitorRecovery() line: 250]
INFOℹ️: Found 2 monitors on RN backup, attempting recovery - WalletViewModel [WalletViewModel.swift: checkForOrphanedChannelMonitorRecovery() line: 268]
DEBUG: Stopping node... [LightningService.swift: stop(clearEventCallback:) line: 260]
INFOℹ️: Channel monitor recovery complete - WalletViewModel [WalletViewModel.swift: checkForOrphanedChannelMonitorRecovery() line: 296]

@jvsena42 jvsena42 marked this pull request as ready for review February 25, 2026 17:06
@jvsena42 jvsena42 requested a review from pwltr February 25, 2026 17:44
@jvsena42 jvsena42 requested a review from ben-kaufman February 26, 2026 09:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

recover force-closed channel funds lost during RN migration

1 participant