Skip to content

HTLC sweep transaction fails to broadcast due to dust output #3831

Open
@whfuyn

Description

@whfuyn

Recently, I encountered a bug that an HTLC is not claimed by either party after the channel is force-closed.

Logs indicate that one node is attempting to claim that HTLC via an htlc-timeout transaction, but failed with RPC error (400) due to dust output.

Here is part of the logs. cf3..0f4:11 is the HTLC to be swept.

2025-06-05T08:02:40.100395Z DEBUG lightning::chain::package:1062: Adding claiming input for outpoint cf3f0c985e6c130f2a3988e888d58d8cc51945efaceb7bfec70cd95ca086d0f4:11
2025-06-05T08:02:40.100719Z  INFO lightning::chain::onchaintx:528: Rebroadcasting onchain HTLC claim tx (0 preimage, 1 timeout, 0 revoked) with txid 2ade336033804fe0fd619a49f7c9dd50c455cc4cbceb8118252d90158de30cca
2025-06-05T08:02:40.101883Z DEBUG ldk_node::chain:1036: Failed to broadcast due to HTTP connection error: RPC error
tx: 02000000000101f4d086a05cd90cc7fe7bebacef4519c58c8dd588e888392a0f136c5e980c3fcf0b00000000010000000200000000000000001600141dec8ff322b271c6fd27d855c2afeeacf868d9e50000000000000000226a2088aa90deac782500cbeb1787dccc9e941ea865a6511dba98f53bac3fa81135bd0347304402200a424a7942f5f7887d6e1a2a8c17430a601ac9f015c3ee974e68dffee5d07677022075b6a389adeaa661ab0ff86b21fd8027f47b5421e6beaa2cea54da913a4a95ef01008d76a914a64d88bc4137d38b7cd5bdd77806f859cacb9bfb8763ac67210299164eef8e0b0fdde0556aa98143dfd7f2ff6fd18728b01b188041b4766d279c7c8201208763a914c797839ad0401e45b721c50a695fb9d241de668a88527c2103646ebe4f487aee44dc9e585301899699cf53645aff81095522da45d7921329f052ae677502db01b175ac6851b27568b6030000
2025-06-05T08:03:09.172879Z DEBUG lightning_transaction_sync::esplora:248: Finished transaction sync at tip 0e3441163f7c09ed394e421e014ce1fb04af05f74b969d7f9c00ea71f5d0d54c in 0ms: 0 confirmed, 0 unconfirmed.
2025-06-05T08:03:09.172909Z  INFO ldk_node::chain:592: Sync of Lightning wallet finished in 0ms.
2025-06-05T08:03:10.238034Z  INFO lightning::chain::onchaintx:513: Triggering rebroadcast/fee-bump for request with inputs [OutPoint { txid: cf3f0c985e6c130f2a3988e888d58d8cc51945efaceb7bfec70cd95ca086d0f4, vout: 11 }]
2025-06-05T08:03:10.238073Z DEBUG lightning::chain::package:1341: Initiating fee rate bump from 1570 s/kWU (1133 s) to 253 s/kWU (182 s) using HighestOfPreviousOrNew strategy
2025-06-05T08:03:10.238076Z DEBUG lightning::chain::package:1372: new feerate 1570 is equal to previous feerate 1570
2025-06-05T08:03:10.238078Z DEBUG lightning::chain::package:1373: new fee: 1133, input amount: 1000
2025-06-05T08:03:10.238078Z DEBUG lightning::chain::package:1374: remaining output amount is 0 

I guess the dust output occurred because the remote party claimed some of its inputs and the node removed them from the sweep transaction without adjusting the feerate.

The sweep transaction before its output becomes dust (ignore the OP_RETURN commitment):

    tx: Transaction {
        version: Version(
            2,
        ),
        lock_time: 940 blocks,
        input: [
            TxIn {
                previous_output: OutPoint {
                    txid: cf3f0c985e6c130f2a3988e888d58d8cc51945efaceb7bfec70cd95ca086d0f4,
                    vout: 8,
                },
                script_sig: Script(),
                sequence: Sequence(0x00000001),
                witness: Witness: {
                    indices: 0,
                    indices_start: 0,
                    witnesses: [
                    ],
                }
                ,
            },
            TxIn {
                previous_output: OutPoint {
                    txid: cf3f0c985e6c130f2a3988e888d58d8cc51945efaceb7bfec70cd95ca086d0f4,
                    vout: 11,
                },
                script_sig: Script(),
                sequence: Sequence(0x00000001),
                witness: Witness: {
                    indices: 0,
                    indices_start: 0,
                    witnesses: [
                    ],
                }
                ,
            },
            TxIn {
                previous_output: OutPoint {
                    txid: cf3f0c985e6c130f2a3988e888d58d8cc51945efaceb7bfec70cd95ca086d0f4,
                    vout: 10,
                },
                script_sig: Script(),
                sequence: Sequence(0x00000001),
                witness: Witness: {
                    indices: 0,
                    indices_start: 0,
                    witnesses: [
                    ],
                }
                ,
            },
        ],
        output: [
            TxOut {
                value: 661 SAT,
                script_pubkey: Script(OP_0 OP_PUSHBYTES_20 1dec8ff322b271c6fd27d855c2afeeacf868d9e5),
            },
            TxOut {
                value: 0 SAT,
                script_pubkey: Script(OP_RETURN OP_PUSHBYTES_32 0143f6b131ea8f65166dcfbe8ad9255068bd7c7459e6418d9bd0901293fca52f),
            },
        ],
    },

HTLCs at vout 8 and 10 were claimed by the remote party

2025-06-03T05:19:25.062327Z DEBUG lightning::chain::onchaintx:1036: Removing claim tracking due to maturation of claim tx for outpoints:
2025-06-03T05:19:25.062328Z DEBUG lightning::chain::onchaintx:1037:  [OutPoint { txid: cf3f0c985e6c130f2a3988e888d58d8cc51945efaceb7bfec70cd95ca086d0f4, vout: 10 }]
2025-06-03T05:19:25.062331Z DEBUG lightning::chain::onchaintx:1036: Removing claim tracking due to maturation of claim tx for outpoints:
2025-06-03T05:19:25.062332Z DEBUG lightning::chain::onchaintx:1037:  [OutPoint { txid: cf3f0c985e6c130f2a3988e888d58d8cc51945efaceb7bfec70cd95ca086d0f4, vout: 8 }]
2025-06-03T05:19:25.062336Z DEBUG lightning::chain::package:1341: Initiating fee rate bump from 1570 s/kWU (1133 s) to 253 s/kWU (182 s) using ForceBump strategy
2025-06-03T05:19:25.062339Z  WARN lightning::chain::package:1388: Can't bump new claiming tx, output amount 0 would end up below dust threshold 294

Because the feerate is unchanged during the following feerate_bump, it bypasses the dust check.

debug_assert!(new_feerate >= previous_feerate);
if new_feerate == previous_feerate {
return Some((new_fee, new_feerate));
}
let min_relay_fee = INCREMENTAL_RELAY_FEE_SAT_PER_1000_WEIGHT * predicted_weight / 1000;
// BIP 125 Opt-in Full Replace-by-Fee Signaling
// * 3. The replacement transaction pays an absolute fee of at least the sum paid by the original transactions.
// * 4. The replacement transaction must also pay for its own bandwidth at or above the rate set by the node's minimum relay fee setting.
let naive_new_fee = new_fee;
let new_fee = cmp::max(new_fee, previous_fee + min_relay_fee);
if new_fee > naive_new_fee {
log_debug!(logger, "Naive fee bump of {}s does not meet min relay fee requirements of {}s", naive_new_fee - previous_fee, min_relay_fee);
}
let remaining_output_amount = input_amounts.saturating_sub(new_fee);
if remaining_output_amount < dust_limit_sats {
log_warn!(logger, "Can't bump new claiming tx, output amount {} would end up below dust threshold {}", remaining_output_amount, dust_limit_sats);
return None;
}

if self.feerate_previous != 0 {
if let Some((new_fee, feerate)) = feerate_bump(
predicted_weight, input_amounts, dust_limit_sats, self.feerate_previous,
feerate_strategy, conf_target, fee_estimator, logger,
) {
return Some((input_amounts.saturating_sub(new_fee), feerate));
}
} else {
if let Some((new_fee, feerate)) = compute_fee_from_spent_amounts(input_amounts, predicted_weight, conf_target, fee_estimator, logger) {
return Some((cmp::max(input_amounts as i64 - new_fee as i64, dust_limit_sats as i64) as u64, feerate));
}
}

The HTLC sweep transaction with dust output:

{
    "result": {
        "txid": "2ade336033804fe0fd619a49f7c9dd50c455cc4cbceb8118252d90158de30cca",
        "hash": "140c48a6b29aedc093955e43308f7078602ffdcdcbeaf9924740ca35e88d0d5b",
        "version": 2,
        "size": 343,
        "vsize": 180,
        "weight": 718,
        "locktime": 950,
        "vin": [
            {
                "txid": "cf3f0c985e6c130f2a3988e888d58d8cc51945efaceb7bfec70cd95ca086d0f4",
                "vout": 11,
                "scriptSig": {
                    "asm": "",
                    "hex": ""
                },
                "txinwitness": [
                    "304402200a424a7942f5f7887d6e1a2a8c17430a601ac9f015c3ee974e68dffee5d07677022075b6a389adeaa661ab0ff86b21fd8027f47b5421e6beaa2cea54da913a4a95ef01",
                    "",
                    "76a914a64d88bc4137d38b7cd5bdd77806f859cacb9bfb8763ac67210299164eef8e0b0fdde0556aa98143dfd7f2ff6fd18728b01b188041b4766d279c7c8201208763a914c797839ad0401e45b721c50a695fb9d241de668a88527c2103646ebe4f487aee44dc9e585301899699cf53645aff81095522da45d7921329f052ae677502db01b175ac6851b27568"
                ],
                "sequence": 1
            }
        ],
        "vout": [
            {
                "value": "0.00000000",
                "n": 0,
                "scriptPubKey": {
                    "asm": "0 1dec8ff322b271c6fd27d855c2afeeacf868d9e5",
                    "desc": "addr(bc1qrhkgluezkfcudlf8mp2u9tlw4nux3k09jlxq4d)#p3wysca3",
                    "hex": "00141dec8ff322b271c6fd27d855c2afeeacf868d9e5",
                    "address": "bc1qrhkgluezkfcudlf8mp2u9tlw4nux3k09jlxq4d",
                    "type": "witness_v0_keyhash"
                }
            },
            {
                "value": "0.00000000",
                "n": 1,
                "scriptPubKey": {
                    "asm": "OP_RETURN 88aa90deac782500cbeb1787dccc9e941ea865a6511dba98f53bac3fa81135bd",
                    "desc": "raw(6a2088aa90deac782500cbeb1787dccc9e941ea865a6511dba98f53bac3fa81135bd)#pap99d6q",
                    "hex": "6a2088aa90deac782500cbeb1787dccc9e941ea865a6511dba98f53bac3fa81135bd",
                    "type": "nulldata"
                }
            }
        ]
    },
    "error": null
}

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