Skip to content

fix: recover orphaned channel monitors from RN backup#802

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

fix: recover orphaned channel monitors from RN backup#802
jvsena42 wants to merge 11 commits intomasterfrom
fix/reimport-channel-monitor

Conversation

@jvsena42
Copy link
Member

@jvsena42 jvsena42 commented Feb 24, 2026

Fixes #799

This PR adds a one-time channel monitor recovery check on app startup for wallets that migrated from React Native. It fetches orphaned channel monitors from the RN remote backup server and feeds them to LDK to sweep any unclaimed force-closed channel funds.

Description

  1. Adds a one-time recovery check that runs after node startup for migrated wallets
  2. Fetches channel monitors from the RN backup server and restarts the node with the recovered migration data so LDK can sweep unclaimed outputs
  3. Skips marking recovery as complete when some monitors fail to download, allowing retry on next startup

Preview

simulate-failure.webm
after-force-close-claim.webm

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
suspend fun retrieveChannelMonitor(channelId: String): ByteArray? = withContext(ioDispatcher) {
      runCatching {
          throw RuntimeException() //Todo dont commit
          val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) ?: throw RNBackupError.NotSetup()
          val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name)

          val bearer = authenticate(mnemonic, passphrase)
          val url = buildUrl(
              method = "retrieve",
              label = "channel_monitor",
              fileGroup = "ldk",
              channelId = channelId,
              network = getNetworkString(),
          )
          val response: HttpResponse = httpClient.get(url) {
              header("Authorization", bearer.bearer)
          }

          if (!response.status.isSuccess()) throw RNBackupError.RequestFailed("Status: ${response.status.value}")

          val encryptedData = response.body<ByteArray>()
          if (encryptedData.isEmpty()) throw RNBackupError.RequestFailed("Retrieved data is empty")

          val encryptionKey = deriveEncryptionKey(mnemonic, passphrase)
          decrypt(encryptedData, encryptionKey).also {
              if (it.isEmpty()) throw RNBackupError.DecryptFailed("Decrypted data is empty")
          }
      }.onFailure { e ->
          Logger.error("Failed to retrieve channel monitor $channelId", e, context = TAG)
      }.getOrNull()
  }
  • 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 RuntimeException() //Todo dont commit
  • Expected: The wallet will claim the force close funds

@jvsena42 jvsena42 self-assigned this Feb 24, 2026
@jvsena42 jvsena42 marked this pull request as ready for review February 24, 2026 14:59
@jvsena42
Copy link
Member Author

Trying to mock a failure scenario

Base automatically changed from fix/channel-monitor-silent-failure to master February 25, 2026 11:22
@jvsena42
Copy link
Member Author

Logs from failure simulation:

2026-02-25 09:18:21.320 29150-29150 APP                     to.bitkit.dev                        E  2026-02-25 12:18:21.320 ERROR   [MigrationService.kt:1317]           Failed to retrieve 2/2 channel monitors after retries: 6aacb230fc421887691db020d977dabc54d9947a626544993c11c4b82971f75c, 450f7485bb4524866737594464ed8115b6a8d0b5d3d942b1d57aee09242f4efd - Migration
2026-02-25 09:18:21.321 29150-29150 APP                     to.bitkit.dev                        W  2026-02-25 12:18:21.320 WARN    [MigrationService.kt:1327]           Channel monitor count mismatch: expected 2, got 0. Some channels may not be recoverable. - Migration
2026-02-25 09:18:39.305 29150-29237 APP                     to.bitkit.dev                        I  2026-02-25 12:18:39.305 INFO    [WalletViewModel.kt:312]             Running one-time channel monitor recovery check - WalletViewModel
2026-02-25 09:18:55.829 29150-29171 APP                     to.bitkit.dev                        E  2026-02-25 12:18:55.829 ERROR   
[MigrationService.kt:1317]           Failed to retrieve 2/2 channel monitors after retries: 6aacb230fc421887691db020d977dabc54d9947a626544993c11c4b82971f75c, 450f7485bb4524866737594464ed8115b6a8d0b5d3d942b1d57aee09242f4efd - Migration
2026-02-25 09:18:55.829 29150-29171 APP                     to.bitkit.dev                        W  2026-02-25 12:18:55.829 WARN    [MigrationService.kt:1327]           Channel monitor count mismatch: expected 2, got 0. Some channels may not be recoverable. - Migration
2026-02-25 09:18:55.829 29150-29171 APP                     to.bitkit.dev                        I  2026-02-25 12:18:55.829 INFO    [WalletViewModel.kt:319]             No channel monitors found on RN backup - WalletViewModel
2026-02-25 09:18:55.829 29150-29171 APP                     to.bitkit.dev                        W  2026-02-25 12:18:55.829 WARN    [WalletViewModel.kt:349]             Some monitors failed to download, will retry on next startup - WalletViewModel

After mining 146 blocks and run the fix:

2026-02-25 09:24:04.476 29490-29637 APP                     to.bitkit.dev                        I  2026-02-25 12:24:04.476 INFO    [WalletViewModel.kt:312]             Running one-time channel monitor recovery check - WalletViewModel
2026-02-25 09:24:19.959 29490-29513 APP                     to.bitkit.dev                        I  2026-02-25 12:24:19.959 INFO    [WalletViewModel.kt:323]             Found 2 monitors on RN backup, attempting recovery - WalletViewModel
2026-02-25 09:24:30.018 29490-29572 LDK                     to.bitkit.dev                        V  2026-02-25 12:24:30.018 TRACE   [lightning::chain::chainmonitor:1369] Got new ChannelMonitor for channel 450f7485bb4524866737594464ed8115b6a8d0b5d3d942b1d57aee09242f4efd
2026-02-25 09:24:30.248 29490-29572 LDK                     to.bitkit.dev                        I  2026-02-25 12:24:30.248 INFO    [lightning::chain::chainmonitor:1383] Persistence of new ChannelMonitor for channel 450f7485bb4524866737594464ed8115b6a8d0b5d3d942b1d57aee09242f4efd completed
2026-02-25 09:24:30.251 29490-29572 LDK                     to.bitkit.dev                        V  2026-02-25 12:24:30.250 TRACE   [lightning::chain::chainmonitor:1369] Got new ChannelMonitor for channel 6aacb230fc421887691db020d977dabc54d9947a626544993c11c4b82971f75c
2026-02-25 09:24:30.478 29490-29572 LDK                     to.bitkit.dev                        I  2026-02-25 12:24:30.477 INFO    [lightning::chain::chainmonitor:1383] Persistence of new ChannelMonitor for channel 6aacb230fc421887691db020d977dabc54d9947a626544993c11c4b82971f75c completed

@jvsena42 jvsena42 requested a review from ovitrif February 25, 2026 12:31
@jvsena42
Copy link
Member Author

This also fixes migration from update flow, but couldn't mock it reliably without trigger a total failure

@jvsena42
Copy link
Member Author

jvsena42 commented Feb 25, 2026

I noticed the app is not handling the UI for ChannelClosed event properly. Should display a sheet and the transfer activity. I'll do it in the next PR

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