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

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