recover force-closed channel funds lost during RN migration #799

Description

@jvsena42

Summary

A channel was force-closed during migration from RN due to the bug fixed in #760. The channel monitor was silently dropped by .mapNotNull() in fetchRNRemoteLdkData(), causing LDK to not recognize the channel on restart. The counterparty force-closed the channel, and the client's to_local output was never swept because the monitor data was missing.

376,523 sats are still sitting unclaimed on-chain.

Channel Details

FieldValue
Channel ID7c9f614b82e0917924e2925284bc6ba9f8932055e57225ffeb804cde35b96c08
Funding Txid086cb935de4c80ebff2572e5552093f8a96bbc845292e2247991e0824b619f7c
Funding Output0
Channel Capacity441,826 sats
Counterparty03816141f1dce7782ec32b66a300783b1d436b19777e7c686ed00115bd4b88ff4b (Blocktank LSP lnd3)

Force Close Transaction

FieldValue
Close Txidb41cb9e5e562014ea2535c193496c099a59086c22ddafaae32c0fd9ba1b4c0d3
Block Height934,477

Outputs

#ValueStatusPurpose
0330 satsSpent (block 934,493)Anchor
1330 satsSpent (block 934,493)Anchor
264,046 satsSpent (block 934,623)LSP's to_remote (swept by lnd3)
3376,523 satsUnspentClient's to_local — never claimed

Root Cause

MigrationService.fetchRNRemoteLdkData() (before #760 fix):

}.mapNotNull { it.await() }

If any channel monitor retrieval failed, it was silently dropped. The monitor was lost, LDK sent a bogus ChannelReestablish, and the counterparty force-closed.

Proposed Solution

The RN remote backup is not wiped after migration — cleanupAfterMigration() only clears local DataStore preferences. Channel monitors should still be on the RN backup server.

One-time recovery check for all affected users

On app startup (post-migration), perform a one-time check against the RN remote backup:

  1. List channel monitors on the RN backup server via rnBackupClient.listFiles(fileGroup = "ldk")
  2. Compare with the monitors LDK Node currently knows about
  3. If there are orphaned monitors on the remote that LDK doesn't have, retrieve them
  4. For each recovered monitor, check if the corresponding funding output has an unswept to_local output on-chain
  5. If claimable funds are found, feed the monitor to LDK to reconstruct state and sweep

This would recover funds for any user affected by the #760 bug, not just this specific case. The check should be gated behind a flag so it only runs once.

Related

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , '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

    recover force-closed channel funds lost during RN migration #799

    Description

    @jvsena42

    Summary

    A channel was force-closed during migration from RN due to the bug fixed in #760. The channel monitor was silently dropped by .mapNotNull() in fetchRNRemoteLdkData(), causing LDK to not recognize the channel on restart. The counterparty force-closed the channel, and the client's to_local output was never swept because the monitor data was missing.

    376,523 sats are still sitting unclaimed on-chain.

    Channel Details

    FieldValue
    Channel ID7c9f614b82e0917924e2925284bc6ba9f8932055e57225ffeb804cde35b96c08
    Funding Txid086cb935de4c80ebff2572e5552093f8a96bbc845292e2247991e0824b619f7c
    Funding Output0
    Channel Capacity441,826 sats
    Counterparty03816141f1dce7782ec32b66a300783b1d436b19777e7c686ed00115bd4b88ff4b (Blocktank LSP lnd3)

    Force Close Transaction

    FieldValue
    Close Txidb41cb9e5e562014ea2535c193496c099a59086c22ddafaae32c0fd9ba1b4c0d3
    Block Height934,477

    Outputs

    #ValueStatusPurpose
    0330 satsSpent (block 934,493)Anchor
    1330 satsSpent (block 934,493)Anchor
    264,046 satsSpent (block 934,623)LSP's to_remote (swept by lnd3)
    3376,523 satsUnspentClient's to_local — never claimed

    Root Cause

    MigrationService.fetchRNRemoteLdkData() (before #760 fix):

    }.mapNotNull { it.await() }

    If any channel monitor retrieval failed, it was silently dropped. The monitor was lost, LDK sent a bogus ChannelReestablish, and the counterparty force-closed.

    Proposed Solution

    The RN remote backup is not wiped after migration — cleanupAfterMigration() only clears local DataStore preferences. Channel monitors should still be on the RN backup server.

    One-time recovery check for all affected users

    On app startup (post-migration), perform a one-time check against the RN remote backup:

    1. List channel monitors on the RN backup server via rnBackupClient.listFiles(fileGroup = "ldk")
    2. Compare with the monitors LDK Node currently knows about
    3. If there are orphaned monitors on the remote that LDK doesn't have, retrieve them
    4. For each recovered monitor, check if the corresponding funding output has an unswept to_local output on-chain
    5. If claimable funds are found, feed the monitor to LDK to reconstruct state and sweep

    This would recover funds for any user affected by the #760 bug, not just this specific case. The check should be gated behind a flag so it only runs once.

    Related

    Metadata

    Metadata

    Assignees

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , '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

      recover force-closed channel funds lost during RN migration #799

      Description

      @jvsena42

      Summary

      A channel was force-closed during migration from RN due to the bug fixed in #760. The channel monitor was silently dropped by .mapNotNull() in fetchRNRemoteLdkData(), causing LDK to not recognize the channel on restart. The counterparty force-closed the channel, and the client's to_local output was never swept because the monitor data was missing.

      376,523 sats are still sitting unclaimed on-chain.

      Channel Details

      FieldValue
      Channel ID7c9f614b82e0917924e2925284bc6ba9f8932055e57225ffeb804cde35b96c08
      Funding Txid086cb935de4c80ebff2572e5552093f8a96bbc845292e2247991e0824b619f7c
      Funding Output0
      Channel Capacity441,826 sats
      Counterparty03816141f1dce7782ec32b66a300783b1d436b19777e7c686ed00115bd4b88ff4b (Blocktank LSP lnd3)

      Force Close Transaction

      FieldValue
      Close Txidb41cb9e5e562014ea2535c193496c099a59086c22ddafaae32c0fd9ba1b4c0d3
      Block Height934,477

      Outputs

      #ValueStatusPurpose
      0330 satsSpent (block 934,493)Anchor
      1330 satsSpent (block 934,493)Anchor
      264,046 satsSpent (block 934,623)LSP's to_remote (swept by lnd3)
      3376,523 satsUnspentClient's to_local — never claimed

      Root Cause

      MigrationService.fetchRNRemoteLdkData() (before #760 fix):

      }.mapNotNull { it.await() }

      If any channel monitor retrieval failed, it was silently dropped. The monitor was lost, LDK sent a bogus ChannelReestablish, and the counterparty force-closed.

      Proposed Solution

      The RN remote backup is not wiped after migration — cleanupAfterMigration() only clears local DataStore preferences. Channel monitors should still be on the RN backup server.

      One-time recovery check for all affected users

      On app startup (post-migration), perform a one-time check against the RN remote backup:

      1. List channel monitors on the RN backup server via rnBackupClient.listFiles(fileGroup = "ldk")
      2. Compare with the monitors LDK Node currently knows about
      3. If there are orphaned monitors on the remote that LDK doesn't have, retrieve them
      4. For each recovered monitor, check if the corresponding funding output has an unswept to_local output on-chain
      5. If claimable funds are found, feed the monitor to LDK to reconstruct state and sweep

      This would recover funds for any user affected by the #760 bug, not just this specific case. The check should be gated behind a flag so it only runs once.

      Related

      Metadata

      Metadata

      Assignees

      Labels

      No labels
      No labels

      Type

      No type

      Projects

      No projects

        Milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , '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

        recover force-closed channel funds lost during RN migration #799

        Description

        @jvsena42

        Summary

        A channel was force-closed during migration from RN due to the bug fixed in #760. The channel monitor was silently dropped by .mapNotNull() in fetchRNRemoteLdkData(), causing LDK to not recognize the channel on restart. The counterparty force-closed the channel, and the client's to_local output was never swept because the monitor data was missing.

        376,523 sats are still sitting unclaimed on-chain.

        Channel Details

        FieldValue
        Channel ID7c9f614b82e0917924e2925284bc6ba9f8932055e57225ffeb804cde35b96c08
        Funding Txid086cb935de4c80ebff2572e5552093f8a96bbc845292e2247991e0824b619f7c
        Funding Output0
        Channel Capacity441,826 sats
        Counterparty03816141f1dce7782ec32b66a300783b1d436b19777e7c686ed00115bd4b88ff4b (Blocktank LSP lnd3)

        Force Close Transaction

        FieldValue
        Close Txidb41cb9e5e562014ea2535c193496c099a59086c22ddafaae32c0fd9ba1b4c0d3
        Block Height934,477

        Outputs

        #ValueStatusPurpose
        0330 satsSpent (block 934,493)Anchor
        1330 satsSpent (block 934,493)Anchor
        264,046 satsSpent (block 934,623)LSP's to_remote (swept by lnd3)
        3376,523 satsUnspentClient's to_local — never claimed

        Root Cause

        MigrationService.fetchRNRemoteLdkData() (before #760 fix):

        }.mapNotNull { it.await() }

        If any channel monitor retrieval failed, it was silently dropped. The monitor was lost, LDK sent a bogus ChannelReestablish, and the counterparty force-closed.

        Proposed Solution

        The RN remote backup is not wiped after migration — cleanupAfterMigration() only clears local DataStore preferences. Channel monitors should still be on the RN backup server.

        One-time recovery check for all affected users

        On app startup (post-migration), perform a one-time check against the RN remote backup:

        1. List channel monitors on the RN backup server via rnBackupClient.listFiles(fileGroup = "ldk")
        2. Compare with the monitors LDK Node currently knows about
        3. If there are orphaned monitors on the remote that LDK doesn't have, retrieve them
        4. For each recovered monitor, check if the corresponding funding output has an unswept to_local output on-chain
        5. If claimable funds are found, feed the monitor to LDK to reconstruct state and sweep

        This would recover funds for any user affected by the #760 bug, not just this specific case. The check should be gated behind a flag so it only runs once.

        Related

        Metadata

        Metadata

        Assignees

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , '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

          recover force-closed channel funds lost during RN migration #799

          Description

          @jvsena42

          Summary

          A channel was force-closed during migration from RN due to the bug fixed in #760. The channel monitor was silently dropped by .mapNotNull() in fetchRNRemoteLdkData(), causing LDK to not recognize the channel on restart. The counterparty force-closed the channel, and the client's to_local output was never swept because the monitor data was missing.

          376,523 sats are still sitting unclaimed on-chain.

          Channel Details

          FieldValue
          Channel ID7c9f614b82e0917924e2925284bc6ba9f8932055e57225ffeb804cde35b96c08
          Funding Txid086cb935de4c80ebff2572e5552093f8a96bbc845292e2247991e0824b619f7c
          Funding Output0
          Channel Capacity441,826 sats
          Counterparty03816141f1dce7782ec32b66a300783b1d436b19777e7c686ed00115bd4b88ff4b (Blocktank LSP lnd3)

          Force Close Transaction

          FieldValue
          Close Txidb41cb9e5e562014ea2535c193496c099a59086c22ddafaae32c0fd9ba1b4c0d3
          Block Height934,477

          Outputs

          #ValueStatusPurpose
          0330 satsSpent (block 934,493)Anchor
          1330 satsSpent (block 934,493)Anchor
          264,046 satsSpent (block 934,623)LSP's to_remote (swept by lnd3)
          3376,523 satsUnspentClient's to_local — never claimed

          Root Cause

          MigrationService.fetchRNRemoteLdkData() (before #760 fix):

          }.mapNotNull { it.await() }

          If any channel monitor retrieval failed, it was silently dropped. The monitor was lost, LDK sent a bogus ChannelReestablish, and the counterparty force-closed.

          Proposed Solution

          The RN remote backup is not wiped after migration — cleanupAfterMigration() only clears local DataStore preferences. Channel monitors should still be on the RN backup server.

          One-time recovery check for all affected users

          On app startup (post-migration), perform a one-time check against the RN remote backup:

          1. List channel monitors on the RN backup server via rnBackupClient.listFiles(fileGroup = "ldk")
          2. Compare with the monitors LDK Node currently knows about
          3. If there are orphaned monitors on the remote that LDK doesn't have, retrieve them
          4. For each recovered monitor, check if the corresponding funding output has an unswept to_local output on-chain
          5. If claimable funds are found, feed the monitor to LDK to reconstruct state and sweep

          This would recover funds for any user affected by the #760 bug, not just this specific case. The check should be gated behind a flag so it only runs once.

          Related

          Metadata

          Metadata

          Assignees

          Labels

          No labels
          No labels

          Type

          No type

          Projects

          No projects

            Milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , '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

            recover force-closed channel funds lost during RN migration #799

            Description

            @jvsena42

            Summary

            A channel was force-closed during migration from RN due to the bug fixed in #760. The channel monitor was silently dropped by .mapNotNull() in fetchRNRemoteLdkData(), causing LDK to not recognize the channel on restart. The counterparty force-closed the channel, and the client's to_local output was never swept because the monitor data was missing.

            376,523 sats are still sitting unclaimed on-chain.

            Channel Details

            FieldValue
            Channel ID7c9f614b82e0917924e2925284bc6ba9f8932055e57225ffeb804cde35b96c08
            Funding Txid086cb935de4c80ebff2572e5552093f8a96bbc845292e2247991e0824b619f7c
            Funding Output0
            Channel Capacity441,826 sats
            Counterparty03816141f1dce7782ec32b66a300783b1d436b19777e7c686ed00115bd4b88ff4b (Blocktank LSP lnd3)

            Force Close Transaction

            FieldValue
            Close Txidb41cb9e5e562014ea2535c193496c099a59086c22ddafaae32c0fd9ba1b4c0d3
            Block Height934,477

            Outputs

            #ValueStatusPurpose
            0330 satsSpent (block 934,493)Anchor
            1330 satsSpent (block 934,493)Anchor
            264,046 satsSpent (block 934,623)LSP's to_remote (swept by lnd3)
            3376,523 satsUnspentClient's to_local — never claimed

            Root Cause

            MigrationService.fetchRNRemoteLdkData() (before #760 fix):

            }.mapNotNull { it.await() }

            If any channel monitor retrieval failed, it was silently dropped. The monitor was lost, LDK sent a bogus ChannelReestablish, and the counterparty force-closed.

            Proposed Solution

            The RN remote backup is not wiped after migration — cleanupAfterMigration() only clears local DataStore preferences. Channel monitors should still be on the RN backup server.

            One-time recovery check for all affected users

            On app startup (post-migration), perform a one-time check against the RN remote backup:

            1. List channel monitors on the RN backup server via rnBackupClient.listFiles(fileGroup = "ldk")
            2. Compare with the monitors LDK Node currently knows about
            3. If there are orphaned monitors on the remote that LDK doesn't have, retrieve them
            4. For each recovered monitor, check if the corresponding funding output has an unswept to_local output on-chain
            5. If claimable funds are found, feed the monitor to LDK to reconstruct state and sweep

            This would recover funds for any user affected by the #760 bug, not just this specific case. The check should be gated behind a flag so it only runs once.

            Related

            Metadata

            Metadata

            Assignees

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , '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

              recover force-closed channel funds lost during RN migration #799

              Description

              @jvsena42

              Summary

              A channel was force-closed during migration from RN due to the bug fixed in #760. The channel monitor was silently dropped by .mapNotNull() in fetchRNRemoteLdkData(), causing LDK to not recognize the channel on restart. The counterparty force-closed the channel, and the client's to_local output was never swept because the monitor data was missing.

              376,523 sats are still sitting unclaimed on-chain.

              Channel Details

              FieldValue
              Channel ID7c9f614b82e0917924e2925284bc6ba9f8932055e57225ffeb804cde35b96c08
              Funding Txid086cb935de4c80ebff2572e5552093f8a96bbc845292e2247991e0824b619f7c
              Funding Output0
              Channel Capacity441,826 sats
              Counterparty03816141f1dce7782ec32b66a300783b1d436b19777e7c686ed00115bd4b88ff4b (Blocktank LSP lnd3)

              Force Close Transaction

              FieldValue
              Close Txidb41cb9e5e562014ea2535c193496c099a59086c22ddafaae32c0fd9ba1b4c0d3
              Block Height934,477

              Outputs

              #ValueStatusPurpose
              0330 satsSpent (block 934,493)Anchor
              1330 satsSpent (block 934,493)Anchor
              264,046 satsSpent (block 934,623)LSP's to_remote (swept by lnd3)
              3376,523 satsUnspentClient's to_local — never claimed

              Root Cause

              MigrationService.fetchRNRemoteLdkData() (before #760 fix):

              }.mapNotNull { it.await() }

              If any channel monitor retrieval failed, it was silently dropped. The monitor was lost, LDK sent a bogus ChannelReestablish, and the counterparty force-closed.

              Proposed Solution

              The RN remote backup is not wiped after migration — cleanupAfterMigration() only clears local DataStore preferences. Channel monitors should still be on the RN backup server.

              One-time recovery check for all affected users

              On app startup (post-migration), perform a one-time check against the RN remote backup:

              1. List channel monitors on the RN backup server via rnBackupClient.listFiles(fileGroup = "ldk")
              2. Compare with the monitors LDK Node currently knows about
              3. If there are orphaned monitors on the remote that LDK doesn't have, retrieve them
              4. For each recovered monitor, check if the corresponding funding output has an unswept to_local output on-chain
              5. If claimable funds are found, feed the monitor to LDK to reconstruct state and sweep

              This would recover funds for any user affected by the #760 bug, not just this specific case. The check should be gated behind a flag so it only runs once.

              Related

              Metadata

              Metadata

              Assignees

              Labels

              No labels
              No labels

              Type

              No type

              Projects

              No projects

                Milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , '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

                recover force-closed channel funds lost during RN migration #799

                Description

                @jvsena42

                Summary

                A channel was force-closed during migration from RN due to the bug fixed in #760. The channel monitor was silently dropped by .mapNotNull() in fetchRNRemoteLdkData(), causing LDK to not recognize the channel on restart. The counterparty force-closed the channel, and the client's to_local output was never swept because the monitor data was missing.

                376,523 sats are still sitting unclaimed on-chain.

                Channel Details

                FieldValue
                Channel ID7c9f614b82e0917924e2925284bc6ba9f8932055e57225ffeb804cde35b96c08
                Funding Txid086cb935de4c80ebff2572e5552093f8a96bbc845292e2247991e0824b619f7c
                Funding Output0
                Channel Capacity441,826 sats
                Counterparty03816141f1dce7782ec32b66a300783b1d436b19777e7c686ed00115bd4b88ff4b (Blocktank LSP lnd3)

                Force Close Transaction

                FieldValue
                Close Txidb41cb9e5e562014ea2535c193496c099a59086c22ddafaae32c0fd9ba1b4c0d3
                Block Height934,477

                Outputs

                #ValueStatusPurpose
                0330 satsSpent (block 934,493)Anchor
                1330 satsSpent (block 934,493)Anchor
                264,046 satsSpent (block 934,623)LSP's to_remote (swept by lnd3)
                3376,523 satsUnspentClient's to_local — never claimed

                Root Cause

                MigrationService.fetchRNRemoteLdkData() (before #760 fix):

                }.mapNotNull { it.await() }

                If any channel monitor retrieval failed, it was silently dropped. The monitor was lost, LDK sent a bogus ChannelReestablish, and the counterparty force-closed.

                Proposed Solution

                The RN remote backup is not wiped after migration — cleanupAfterMigration() only clears local DataStore preferences. Channel monitors should still be on the RN backup server.

                One-time recovery check for all affected users

                On app startup (post-migration), perform a one-time check against the RN remote backup:

                1. List channel monitors on the RN backup server via rnBackupClient.listFiles(fileGroup = "ldk")
                2. Compare with the monitors LDK Node currently knows about
                3. If there are orphaned monitors on the remote that LDK doesn't have, retrieve them
                4. For each recovered monitor, check if the corresponding funding output has an unswept to_local output on-chain
                5. If claimable funds are found, feed the monitor to LDK to reconstruct state and sweep

                This would recover funds for any user affected by the #760 bug, not just this specific case. The check should be gated behind a flag so it only runs once.

                Related

                Metadata

                Metadata

                Assignees

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions