Skip to content

Force close transactions with BlockTank not being confirmed #275

@jvsena42

Description

@jvsena42

What I got so far:

  • Blocktank node is a trustedLnPeers
  • Trusted peer channels use anchor outputs
  • Anchor outputs have a different outputs scripts with different CSV conditions
DEBUG: Current block height: 31732
WARN⚠️: 🔑 CLAIMABLE HEIGHT: 0 ← Output becomes spendable at this block
WARN⚠️: ⚠️ Claimable height is 0 - this may indicate LDK hasn't calculated the timelock yet

LDK has claimableHeight = 0, which means LDK is failing to calculate when this force-close output becomes spendable. This is why the funds are stuck indefinitely.

External node channels are not on trusted list so they use a non-anchor channel format. That is why it worked on the tests

Another strange this is that transactionName = 0
It suggests LDK might be completely missing the force-close transaction details.
It would be helpful is I could see the commitment transaction in the mempool

WARN⚠️:   📄 TRANSACTION NAME/ID: 0
WARN⚠️:   🔑 CLAIMABLE HEIGHT: 0 ← Output becomes spendable at this block
WARN⚠️:   ⚠️ Claimable height is 0 - this may indicate LDK hasn't calculated the timelock yet
WARN⚠️:   🔍 ANCHOR CHANNEL BUG: LDK cannot determine when CSV timelock expires

    func sync() async throws {
        guard let node else {
            throw AppError(serviceError: .nodeNotSetup)
        }

        Logger.debug("Syncing LDK...")
        try await ServiceQueue.background(.ldk) {
            try node.syncWallets()
            // try? self.setMaxDustHtlcExposureForCurrentChannels()
        }
        Logger.info("LDK synced")

        await refreshChannelCache()

        // Log detailed balance information after sync
        let balanceDetails = node.listBalances()
        let nodeStatus = node.status()
        let currentHeight = nodeStatus.currentBestBlock.height

        Logger.debug("=== LDK Balance Details After Sync ===")
        Logger.debug("Current block height: \(currentHeight)")
        Logger.debug("Total onchain: \(balanceDetails.totalOnchainBalanceSats) sats")
        Logger.debug("Total lightning: \(balanceDetails.totalLightningBalanceSats) sats")
        Logger.debug("Lightning balances count: \(balanceDetails.lightningBalances.count)")

        for (index, balance) in balanceDetails.lightningBalances.enumerated() {
            Logger.debug("Lightning Balance #\(index + 1):")

            switch balance {
            case let .claimableOnChannelClose(
                channelId,
                counterpartyNodeId,
                amountSat,
                transactionName,
                confirmationHeight,
                claimableHeight,
                paymentHash,
                paymentPreimage
            ):
                Logger.warn("  Type: ClaimableOnChannelClose")
                Logger.warn("  ⚠️ This is a FORCE-CLOSED channel balance waiting to be claimed!")
                Logger.debug("  Channel ID: \(channelId)")
                Logger.debug("  Amount: \(amountSat) sats")
                Logger.warn("  📄 TRANSACTION NAME/ID: \(transactionName)")
                Logger.debug("  Confirmation height: \(confirmationHeight)")
                Logger.warn("  🔑 CLAIMABLE HEIGHT: \(claimableHeight) ← Output becomes spendable at this block")

                // Calculate blocks remaining until claimable
                let currentHeightUInt64 = UInt64(currentHeight)
                if claimableHeight > currentHeightUInt64 {
                    let blocksRemaining = claimableHeight - currentHeightUInt64
                    Logger.warn("  ⏰ Blocks until claimable: \(blocksRemaining) (need to mine \(blocksRemaining) more blocks)")
                } else if claimableHeight > 0 {
                    Logger.warn("  ✅ Output is NOW CLAIMABLE (claimable height \(claimableHeight) <= current height \(currentHeight))")
                    Logger.warn("  🚨 LDK should sweep this automatically - if it hasn't, there may be an issue")
                } else {
                    Logger.warn("  ⚠️ Claimable height is 0 - this may indicate LDK hasn't calculated the timelock yet")
                    Logger.warn("  🔍 ANCHOR CHANNEL BUG: LDK cannot determine when CSV timelock expires")
                    Logger.warn("  💡 This is a known issue with anchor channels - funds are stuck until LDK is fixed or manual intervention")
                }

                if paymentHash != 0 {
                    Logger.debug("  Payment hash: \(paymentHash)")
                }
                if paymentPreimage != 0 {
                    Logger.debug("  Payment preimage: \(paymentPreimage)")
                }
                Logger.debug("  Counterparty: \(counterpartyNodeId)")

            case let .claimableAwaitingConfirmations(channelId, counterpartyNodeId, amountSat, confirmationHeight, transactionName):
                Logger.info("  Type: ClaimableAwaitingConfirmations")
                Logger.info("  ⏳ Sweep transaction is pending, waiting for confirmations")
                Logger.debug("  Transaction: \(transactionName)")
                Logger.debug("  Confirmation height: \(confirmationHeight)")
                Logger.debug("  Counterparty: \(counterpartyNodeId)")

            case .contentiousClaimable:
                Logger.warn("  Type: ContentiousClaimable")
            case .maybeTimeoutClaimableHtlc:
                Logger.debug("  Type: MaybeTimeoutClaimableHTLC")
            case .maybePreimageClaimableHtlc:
                Logger.debug("  Type: MaybePreimageClaimableHTLC")
            case .counterpartyRevokedOutputClaimable:
                Logger.warn("  Type: CounterpartyRevokedOutputClaimable")
            }
        }
        Logger.debug("=====================================")

        // Emit state change with sync timestamp from node status
        if let latestSyncTimestamp = nodeStatus.latestLightningWalletSyncTimestamp {
            let syncTimestamp = UInt64(latestSyncTimestamp)
            syncStatusChangedSubject.send(syncTimestamp)
        } else {
            let syncTimestamp = UInt64(Date().timeIntervalSince1970)
            syncStatusChangedSubject.send(syncTimestamp)
        }
    }

Originally posted by @jvsena42 in #207 (comment)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions