I just noticed this behavior after upgrading to the 26.06.7 from 25.12.
I'm seeing something like this:
"status": [
"CHANNELD_NORMAL:Reconnected, and reestablished.",
"CLOSINGD_SIGEXCHANGE:We agreed on a closing fee of 195 satoshi for tx:777777",
"ONCHAIN:Tracking our own unilateral close",
"ONCHAIN:1 outputs unresolved: in 1283 blocks will spend DELAYED_OUTPUT_TO_US (77777:3) using OUR_DELAYED_RETURN_TO_WALLET"
],
indicating that we agreed on the fee, but my node did not sign the mutual close tx and instead fired force close. I have 2 of these now in my channel tracking.
I'll just just dump the analysis here:
Yes, this is indeed a genuine architectural bug / design flaw in Core Lightning.
You are completely right to call it a bug. When looking directly at the Core Lightning C codebase, the exact flaw and why it happens after recent releases is clear:
Where in the Code It Goes Wrong
The bug is located in
lightningd/closing_control.c
:
- The "Silent Rejection" Bug (closing_control.c:L270-L281)
When closingd receives the closing signature from the peer, it passes it to the master daemon via peer_received_closing_signature:
/* lightningd/closing_control.c */
static void peer_received_closing_signature(struct channel *channel, const u8 *msg)
{
...
if (closing_fee_is_acceptable(ld, channel, tx)) {
channel_set_last_tx(channel, tx, &sig);
wallet_channel_save(ld->wallet, channel);
}
// Send back the txid so we can update the billboard on selection.
bitcoin_txid(channel->last_tx, &tx_id);
/* OK, you can continue now. */
subd_send_msg(channel->owner,
take(towire_closingd_received_signature_reply(channel, &tx_id)));
}
Look closely at what happens if closing_fee_is_acceptable(ld, channel, tx) returns false:
There is no else statement.
It does not fail the negotiation.
It does not send an error back to closingd.
It silently does not update channel->last_tx.
It reads channel->last_tx (which is still your unilateral commitment transaction!) and sends that txid back to closingd.
closingd believes everything succeeded, prints the billboard: "We agreed on a closing fee of 195 satoshi for tx:<commitment_txid>" and terminates with WIRE_CLOSINGD_COMPLETE.
2. The Blind Broadcast in drop_to_chain (peer_control.c:L480)
When closingd completes, lightningd calls:
/* Channel gets dropped to chain cooperatively. */
drop_to_chain(channel->peer->ld, channel, true, NULL);
Inside drop_to_chain:
tal_arr_expand(&txs, sign_and_send_last(tmpctx, ld, channel, cmd_id,
channel->last_tx,
&channel->last_sig));
drop_to_chain blindly signs and broadcasts whatever is in channel->last_tx. Because channel->last_tx was never updated to the mutual close transaction, it signs and broadcasts your unilateral commitment transaction, turning what it thought was a cooperative close into an accidental force close!
Why Did closingd and lightningd Disagree on 195 Sats?
You might wonder: If 195 sat was unacceptable to lightningd, why did closingd propose/accept it in the first place?
This is the second part of the bug:
When closingd starts, it calculates the minimum fee using a pre-calculated weight estimate:
/* closingd/closingd.c:L875 */
calc_fee_bounds(closing_tx_weight_estimate(...), min_feerate, ...)
But in lightningd, closing_fee_is_acceptable recalculates the fee using the actual measured weight of the received transaction:
/* lightningd/closing_control.c:L211 */
weight = bitcoin_tx_weight(tx) + bitcoin_tx_2of2_input_witness_weight();
min_fee = amount_tx_fee(min_feerate, weight);
if (amount_sat_less(fee, min_fee))
return false;
If the peer’s address script was slightly larger than the dummy estimate (e.g. Taproot vs SegWit), or if your Bitcoin node updated min_feerate by even 1 satoshi while negotiation was in progress:
- closingd thought 195 sats was ≥ min_fee
- lightningd calculated min_fee = 196 sats
- 195 < 196 → closing_fee_is_acceptable returned false!
Why Did You Only See This After the Latest Release?
Two major changes in recent CLN releases made this bug surface much more frequently:
Zero-Fee HTLC Anchors Became Standard (option_anchors_zero_fee_htlc_tx): Notice this line in closing_control.c:L498:
/* Always use quickclose with anchors */
|| option_anchor_outputs
|| option_anchors_zero_fee_htlc_tx,
In older releases without anchors, CLN used multi-step iterative negotiation (fee_negotiation_step). On anchor channels, CLN forces quickclose, where both nodes immediately jump to the boundary of their fee range. This causes the agreed fee to land right on the razor's edge of the minimum relay fee floor, where 1 satoshi of rounding discrepancy triggers this exact failure.
Feerate Floor Enforcement: Recent versions aligned the fee floor strictly with Bitcoin Core v27/v28 mempool minimums, causing low-feerate mutual close proposals to be rejected by lightningd much more aggressively.
What CLN Should Do (The Fix)
Instead of silently falling through, if closing_fee_is_acceptable is false:
- lightningd should either instruct closingd that the fee is below the floor so it bumps the offer, or
- Explicitly abort with an error rather than silently broadcasting an obsolete commitment transaction.
I just noticed this behavior after upgrading to the 26.06.7 from 25.12.
I'm seeing something like this:
indicating that we agreed on the fee, but my node did not sign the mutual close tx and instead fired force close. I have 2 of these now in my channel tracking.
I'll just just dump the analysis here:
Yes, this is indeed a genuine architectural bug / design flaw in Core Lightning.
You are completely right to call it a bug. When looking directly at the Core Lightning C codebase, the exact flaw and why it happens after recent releases is clear:
Where in the Code It Goes Wrong
The bug is located in
lightningd/closing_control.c
:
When closingd receives the closing signature from the peer, it passes it to the master daemon via peer_received_closing_signature:
Look closely at what happens if closing_fee_is_acceptable(ld, channel, tx) returns false:
There is no else statement.
It does not fail the negotiation.
It does not send an error back to closingd.
It silently does not update channel->last_tx.
It reads channel->last_tx (which is still your unilateral commitment transaction!) and sends that txid back to closingd.
closingd believes everything succeeded, prints the billboard: "We agreed on a closing fee of 195 satoshi for tx:<commitment_txid>" and terminates with WIRE_CLOSINGD_COMPLETE.
2. The Blind Broadcast in drop_to_chain (peer_control.c:L480)
When closingd completes, lightningd calls:
Inside drop_to_chain:
drop_to_chain blindly signs and broadcasts whatever is in channel->last_tx. Because channel->last_tx was never updated to the mutual close transaction, it signs and broadcasts your unilateral commitment transaction, turning what it thought was a cooperative close into an accidental force close!
Why Did closingd and lightningd Disagree on 195 Sats?
You might wonder: If 195 sat was unacceptable to lightningd, why did closingd propose/accept it in the first place?
This is the second part of the bug:
When closingd starts, it calculates the minimum fee using a pre-calculated weight estimate:
But in lightningd, closing_fee_is_acceptable recalculates the fee using the actual measured weight of the received transaction:
If the peer’s address script was slightly larger than the dummy estimate (e.g. Taproot vs SegWit), or if your Bitcoin node updated min_feerate by even 1 satoshi while negotiation was in progress:
Why Did You Only See This After the Latest Release?
Two major changes in recent CLN releases made this bug surface much more frequently:
Zero-Fee HTLC Anchors Became Standard (option_anchors_zero_fee_htlc_tx): Notice this line in closing_control.c:L498:
In older releases without anchors, CLN used multi-step iterative negotiation (fee_negotiation_step). On anchor channels, CLN forces quickclose, where both nodes immediately jump to the boundary of their fee range. This causes the agreed fee to land right on the razor's edge of the minimum relay fee floor, where 1 satoshi of rounding discrepancy triggers this exact failure.
Feerate Floor Enforcement: Recent versions aligned the fee floor strictly with Bitcoin Core v27/v28 mempool minimums, causing low-feerate mutual close proposals to be rejected by lightningd much more aggressively.
What CLN Should Do (The Fix)
Instead of silently falling through, if closing_fee_is_acceptable is false: