fix: recover orphaned channel monitors from RN backup - #802

Merged
ovitrif merged 48 commits into
masterfrom
fix/reimport-channel-monitor
Mar 4, 2026
Merged

fix: recover orphaned channel monitors from RN backup#802
ovitrif merged 48 commits into
masterfrom
fix/reimport-channel-monitor

Conversation

@jvsena42

@jvsena42jvsena42 commented Feb 24, 2026

Copy link
Copy Markdown
Member

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

QA Notes backported from stacked PR #806

for counterparty closing use the slack command /regtest-ln-close-channel <fundingTx:vout> [force]

  1. Counterparty force close detection

    • In regtest, open a channel with the LSP
    • Force close the channel from the LSP side
    • Mine 1 block
    • Verify the "Connection Closed" sheet appears
    • Mine 146 blocks
    • Verify a transfer banner appears on the savings screen tracking the sweep
  2. ** Counterparty cooperative close detection**

    • Trigger a cooperative close from the counterparty
    • Verify the transfer is created
    • Verify the spending balance is imediately subtracted
  3. Cooperative close detection

    • Transfer to spending flow
    • Verify the transfer is created
    • Verify the spending balance is imediately subtracted

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

Copy link
Copy Markdown
MemberAuthor

Trying to mock a failure scenario

Base automatically changed from fix/channel-monitor-silent-failure to masterFebruary 25, 2026 11:22
@jvsena42

Copy link
Copy Markdown
MemberAuthor

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

Copy link
Copy Markdown
MemberAuthor

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

@jvsena42

This comment was marked as resolved.

@jvsena42

Copy link
Copy Markdown
MemberAuthor

Better testing on #806 branch

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tAck 👏🏻

will :shipit:

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
@ovitrif
ovitrif disabled auto-merge March 4, 2026 12:53
@ovitrif
ovitrif merged commit e6dcb4b into masterMar 4, 2026
18 checks passed
@ovitrif
ovitrif deleted the fix/reimport-channel-monitor branch March 4, 2026 12:54
@ovitrifovitrif added this to the 2.1.0 milestone Mar 9, 2026
Sign up for freeto 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

2 participants

@jvsena42@ovitrif
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix: recover orphaned channel monitors from RN backup - #802

Merged
ovitrif merged 48 commits into
masterfrom
fix/reimport-channel-monitor
Mar 4, 2026
Merged

fix: recover orphaned channel monitors from RN backup#802
ovitrif merged 48 commits into
masterfrom
fix/reimport-channel-monitor

Conversation

@jvsena42

@jvsena42jvsena42 commented Feb 24, 2026

Copy link
Copy Markdown
Member

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

QA Notes backported from stacked PR #806

for counterparty closing use the slack command /regtest-ln-close-channel <fundingTx:vout> [force]

  1. Counterparty force close detection

    • In regtest, open a channel with the LSP
    • Force close the channel from the LSP side
    • Mine 1 block
    • Verify the "Connection Closed" sheet appears
    • Mine 146 blocks
    • Verify a transfer banner appears on the savings screen tracking the sweep
  2. ** Counterparty cooperative close detection**

    • Trigger a cooperative close from the counterparty
    • Verify the transfer is created
    • Verify the spending balance is imediately subtracted
  3. Cooperative close detection

    • Transfer to spending flow
    • Verify the transfer is created
    • Verify the spending balance is imediately subtracted

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

Copy link
Copy Markdown
MemberAuthor

Trying to mock a failure scenario

Base automatically changed from fix/channel-monitor-silent-failure to masterFebruary 25, 2026 11:22
@jvsena42

Copy link
Copy Markdown
MemberAuthor

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

Copy link
Copy Markdown
MemberAuthor

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

@jvsena42

This comment was marked as resolved.

@jvsena42

Copy link
Copy Markdown
MemberAuthor

Better testing on #806 branch

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tAck 👏🏻

will :shipit:

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
@ovitrif
ovitrif disabled auto-merge March 4, 2026 12:53
@ovitrif
ovitrif merged commit e6dcb4b into masterMar 4, 2026
18 checks passed
@ovitrif
ovitrif deleted the fix/reimport-channel-monitor branch March 4, 2026 12:54
@ovitrifovitrif added this to the 2.1.0 milestone Mar 9, 2026
Sign up for freeto 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

2 participants

@jvsena42@ovitrif
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: recover orphaned channel monitors from RN backup - #802

Merged
ovitrif merged 48 commits into
masterfrom
fix/reimport-channel-monitor
Mar 4, 2026
Merged

fix: recover orphaned channel monitors from RN backup#802
ovitrif merged 48 commits into
masterfrom
fix/reimport-channel-monitor

Conversation

@jvsena42

@jvsena42jvsena42 commented Feb 24, 2026

Copy link
Copy Markdown
Member

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

QA Notes backported from stacked PR #806

for counterparty closing use the slack command /regtest-ln-close-channel <fundingTx:vout> [force]

  1. Counterparty force close detection

    • In regtest, open a channel with the LSP
    • Force close the channel from the LSP side
    • Mine 1 block
    • Verify the "Connection Closed" sheet appears
    • Mine 146 blocks
    • Verify a transfer banner appears on the savings screen tracking the sweep
  2. ** Counterparty cooperative close detection**

    • Trigger a cooperative close from the counterparty
    • Verify the transfer is created
    • Verify the spending balance is imediately subtracted
  3. Cooperative close detection

    • Transfer to spending flow
    • Verify the transfer is created
    • Verify the spending balance is imediately subtracted

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

Copy link
Copy Markdown
MemberAuthor

Trying to mock a failure scenario

Base automatically changed from fix/channel-monitor-silent-failure to masterFebruary 25, 2026 11:22
@jvsena42

Copy link
Copy Markdown
MemberAuthor

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

Copy link
Copy Markdown
MemberAuthor

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

@jvsena42

This comment was marked as resolved.

@jvsena42

Copy link
Copy Markdown
MemberAuthor

Better testing on #806 branch

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tAck 👏🏻

will :shipit:

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
@ovitrif
ovitrif disabled auto-merge March 4, 2026 12:53
@ovitrif
ovitrif merged commit e6dcb4b into masterMar 4, 2026
18 checks passed
@ovitrif
ovitrif deleted the fix/reimport-channel-monitor branch March 4, 2026 12:54
@ovitrifovitrif added this to the 2.1.0 milestone Mar 9, 2026
Sign up for freeto 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

2 participants

@jvsena42@ovitrif
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: recover orphaned channel monitors from RN backup - #802

Merged
ovitrif merged 48 commits into
masterfrom
fix/reimport-channel-monitor
Mar 4, 2026
Merged

fix: recover orphaned channel monitors from RN backup#802
ovitrif merged 48 commits into
masterfrom
fix/reimport-channel-monitor

Conversation

@jvsena42

@jvsena42jvsena42 commented Feb 24, 2026

Copy link
Copy Markdown
Member

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

QA Notes backported from stacked PR #806

for counterparty closing use the slack command /regtest-ln-close-channel <fundingTx:vout> [force]

  1. Counterparty force close detection

    • In regtest, open a channel with the LSP
    • Force close the channel from the LSP side
    • Mine 1 block
    • Verify the "Connection Closed" sheet appears
    • Mine 146 blocks
    • Verify a transfer banner appears on the savings screen tracking the sweep
  2. ** Counterparty cooperative close detection**

    • Trigger a cooperative close from the counterparty
    • Verify the transfer is created
    • Verify the spending balance is imediately subtracted
  3. Cooperative close detection

    • Transfer to spending flow
    • Verify the transfer is created
    • Verify the spending balance is imediately subtracted

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

Copy link
Copy Markdown
MemberAuthor

Trying to mock a failure scenario

Base automatically changed from fix/channel-monitor-silent-failure to masterFebruary 25, 2026 11:22
@jvsena42

Copy link
Copy Markdown
MemberAuthor

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

Copy link
Copy Markdown
MemberAuthor

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

@jvsena42

This comment was marked as resolved.

@jvsena42

Copy link
Copy Markdown
MemberAuthor

Better testing on #806 branch

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tAck 👏🏻

will :shipit:

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
@ovitrif
ovitrif disabled auto-merge March 4, 2026 12:53
@ovitrif
ovitrif merged commit e6dcb4b into masterMar 4, 2026
18 checks passed
@ovitrif
ovitrif deleted the fix/reimport-channel-monitor branch March 4, 2026 12:54
@ovitrifovitrif added this to the 2.1.0 milestone Mar 9, 2026
Sign up for freeto 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

2 participants

@jvsena42@ovitrif
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix: recover orphaned channel monitors from RN backup - #802

Merged
ovitrif merged 48 commits into
masterfrom
fix/reimport-channel-monitor
Mar 4, 2026
Merged

fix: recover orphaned channel monitors from RN backup#802
ovitrif merged 48 commits into
masterfrom
fix/reimport-channel-monitor

Conversation

@jvsena42

@jvsena42jvsena42 commented Feb 24, 2026

Copy link
Copy Markdown
Member

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

QA Notes backported from stacked PR #806

for counterparty closing use the slack command /regtest-ln-close-channel <fundingTx:vout> [force]

  1. Counterparty force close detection

    • In regtest, open a channel with the LSP
    • Force close the channel from the LSP side
    • Mine 1 block
    • Verify the "Connection Closed" sheet appears
    • Mine 146 blocks
    • Verify a transfer banner appears on the savings screen tracking the sweep
  2. ** Counterparty cooperative close detection**

    • Trigger a cooperative close from the counterparty
    • Verify the transfer is created
    • Verify the spending balance is imediately subtracted
  3. Cooperative close detection

    • Transfer to spending flow
    • Verify the transfer is created
    • Verify the spending balance is imediately subtracted

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

Copy link
Copy Markdown
MemberAuthor

Trying to mock a failure scenario

Base automatically changed from fix/channel-monitor-silent-failure to masterFebruary 25, 2026 11:22
@jvsena42

Copy link
Copy Markdown
MemberAuthor

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

Copy link
Copy Markdown
MemberAuthor

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

@jvsena42

This comment was marked as resolved.

@jvsena42

Copy link
Copy Markdown
MemberAuthor

Better testing on #806 branch

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tAck 👏🏻

will :shipit:

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
@ovitrif
ovitrif disabled auto-merge March 4, 2026 12:53
@ovitrif
ovitrif merged commit e6dcb4b into masterMar 4, 2026
18 checks passed
@ovitrif
ovitrif deleted the fix/reimport-channel-monitor branch March 4, 2026 12:54
@ovitrifovitrif added this to the 2.1.0 milestone Mar 9, 2026
Sign up for freeto 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

2 participants

@jvsena42@ovitrif
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: recover orphaned channel monitors from RN backup - #802

Merged
ovitrif merged 48 commits into
masterfrom
fix/reimport-channel-monitor
Mar 4, 2026
Merged

fix: recover orphaned channel monitors from RN backup#802
ovitrif merged 48 commits into
masterfrom
fix/reimport-channel-monitor

Conversation

@jvsena42

@jvsena42jvsena42 commented Feb 24, 2026

Copy link
Copy Markdown
Member

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

QA Notes backported from stacked PR #806

for counterparty closing use the slack command /regtest-ln-close-channel <fundingTx:vout> [force]

  1. Counterparty force close detection

    • In regtest, open a channel with the LSP
    • Force close the channel from the LSP side
    • Mine 1 block
    • Verify the "Connection Closed" sheet appears
    • Mine 146 blocks
    • Verify a transfer banner appears on the savings screen tracking the sweep
  2. ** Counterparty cooperative close detection**

    • Trigger a cooperative close from the counterparty
    • Verify the transfer is created
    • Verify the spending balance is imediately subtracted
  3. Cooperative close detection

    • Transfer to spending flow
    • Verify the transfer is created
    • Verify the spending balance is imediately subtracted

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

Copy link
Copy Markdown
MemberAuthor

Trying to mock a failure scenario

Base automatically changed from fix/channel-monitor-silent-failure to masterFebruary 25, 2026 11:22
@jvsena42

Copy link
Copy Markdown
MemberAuthor

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

Copy link
Copy Markdown
MemberAuthor

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

@jvsena42

This comment was marked as resolved.

@jvsena42

Copy link
Copy Markdown
MemberAuthor

Better testing on #806 branch

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tAck 👏🏻

will :shipit:

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
@ovitrif
ovitrif disabled auto-merge March 4, 2026 12:53
@ovitrif
ovitrif merged commit e6dcb4b into masterMar 4, 2026
18 checks passed
@ovitrif
ovitrif deleted the fix/reimport-channel-monitor branch March 4, 2026 12:54
@ovitrifovitrif added this to the 2.1.0 milestone Mar 9, 2026
Sign up for freeto 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

2 participants

@jvsena42@ovitrif
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: recover orphaned channel monitors from RN backup - #802

Merged
ovitrif merged 48 commits into
masterfrom
fix/reimport-channel-monitor
Mar 4, 2026
Merged

fix: recover orphaned channel monitors from RN backup#802
ovitrif merged 48 commits into
masterfrom
fix/reimport-channel-monitor

Conversation

@jvsena42

@jvsena42jvsena42 commented Feb 24, 2026

Copy link
Copy Markdown
Member

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

QA Notes backported from stacked PR #806

for counterparty closing use the slack command /regtest-ln-close-channel <fundingTx:vout> [force]

  1. Counterparty force close detection

    • In regtest, open a channel with the LSP
    • Force close the channel from the LSP side
    • Mine 1 block
    • Verify the "Connection Closed" sheet appears
    • Mine 146 blocks
    • Verify a transfer banner appears on the savings screen tracking the sweep
  2. ** Counterparty cooperative close detection**

    • Trigger a cooperative close from the counterparty
    • Verify the transfer is created
    • Verify the spending balance is imediately subtracted
  3. Cooperative close detection

    • Transfer to spending flow
    • Verify the transfer is created
    • Verify the spending balance is imediately subtracted

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

Copy link
Copy Markdown
MemberAuthor

Trying to mock a failure scenario

Base automatically changed from fix/channel-monitor-silent-failure to masterFebruary 25, 2026 11:22
@jvsena42

Copy link
Copy Markdown
MemberAuthor

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

Copy link
Copy Markdown
MemberAuthor

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

@jvsena42

This comment was marked as resolved.

@jvsena42

Copy link
Copy Markdown
MemberAuthor

Better testing on #806 branch

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tAck 👏🏻

will :shipit:

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
@ovitrif
ovitrif disabled auto-merge March 4, 2026 12:53
@ovitrif
ovitrif merged commit e6dcb4b into masterMar 4, 2026
18 checks passed
@ovitrif
ovitrif deleted the fix/reimport-channel-monitor branch March 4, 2026 12:54
@ovitrifovitrif added this to the 2.1.0 milestone Mar 9, 2026
Sign up for freeto 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

2 participants

@jvsena42@ovitrif
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix: recover orphaned channel monitors from RN backup - #802

Merged
ovitrif merged 48 commits into
masterfrom
fix/reimport-channel-monitor
Mar 4, 2026
Merged

fix: recover orphaned channel monitors from RN backup#802
ovitrif merged 48 commits into
masterfrom
fix/reimport-channel-monitor

Conversation

@jvsena42

@jvsena42jvsena42 commented Feb 24, 2026

Copy link
Copy Markdown
Member

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

QA Notes backported from stacked PR #806

for counterparty closing use the slack command /regtest-ln-close-channel <fundingTx:vout> [force]

  1. Counterparty force close detection

    • In regtest, open a channel with the LSP
    • Force close the channel from the LSP side
    • Mine 1 block
    • Verify the "Connection Closed" sheet appears
    • Mine 146 blocks
    • Verify a transfer banner appears on the savings screen tracking the sweep
  2. ** Counterparty cooperative close detection**

    • Trigger a cooperative close from the counterparty
    • Verify the transfer is created
    • Verify the spending balance is imediately subtracted
  3. Cooperative close detection

    • Transfer to spending flow
    • Verify the transfer is created
    • Verify the spending balance is imediately subtracted

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

Copy link
Copy Markdown
MemberAuthor

Trying to mock a failure scenario

Base automatically changed from fix/channel-monitor-silent-failure to masterFebruary 25, 2026 11:22
@jvsena42

Copy link
Copy Markdown
MemberAuthor

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

Copy link
Copy Markdown
MemberAuthor

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

@jvsena42

This comment was marked as resolved.

@jvsena42

Copy link
Copy Markdown
MemberAuthor

Better testing on #806 branch

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tAck 👏🏻

will :shipit:

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
@ovitrif
ovitrif disabled auto-merge March 4, 2026 12:53
@ovitrif
ovitrif merged commit e6dcb4b into masterMar 4, 2026
18 checks passed
@ovitrif
ovitrif deleted the fix/reimport-channel-monitor branch March 4, 2026 12:54
@ovitrifovitrif added this to the 2.1.0 milestone Mar 9, 2026
Sign up for freeto 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

2 participants

@jvsena42@ovitrif