Skip to content

lightningd: count the witness stack element when sizing onchain spends - #49

Open
davelund110 wants to merge 1 commit into
privkeyio:blake2b-unifiedfrom
davelund110:onchain-witness-weight
Open

davelund110 wants to merge 1 commit into
privkeyio:blake2b-unifiedfrom
davelund110:onchain-witness-weight

Conversation

@davelund110

@davelund110 davelund110 commented Oct 17, 2026 •

Copy link
Copy Markdown

What

onchaind_tx_unsigned() sizes an onchain spend before signing it, so it can pick the fee. Its estimate covers the unsigned transaction, a worst-case signature and the witness script. It leaves out the stack element that sits between the signature and the script:

  • the revocation pubkey, in a penalty for an HTLC output (33 bytes);
  • the preimage, in an HTLC fulfill (32 bytes).

The fee then comes out about 5% short of the feerate it was meant to pay. This PR adds the element's length to the estimate. It's one line, plus a test.

Why it matters

The estimate is 30 to 31 weight units short (the missing element, less the 2 bytes the 73-byte worst-case signature over-counts, since signatures are ground to low-R), about 5%. Whenever the feerate chosen is bitcoind's own minimum, that 5% puts the transaction below it, and bitcoind refuses it every time it is rebroadcast. That happens:

  • when the deadline is 101 to 200 blocks away, where feerate_for_target() uses get_feerate_floor() by design, whatever the estimates say. An HTLC penalty's deadline is to_self_delay (144 by default) after the close, so its first ~44 broadcasts always fall in this window;
  • when estimates sit at the floor, as on a quiet network or a regtest;
  • with a node whose minimum is about 1 sat/vbyte, as Bitcoin Knots' is by default, or with any node once a full mempool raises mempoolminfee (the error is then "mempool min fee not met").

On our regtest:

plugin-bcli: min relay fee not met, 148 < 155
  • HTLC penalty: the cheater's own HTLC-timeout (or HTLC-success) transaction can confirm first. We then have to penalise its output instead, which works but takes longer and leaves more room for things to go wrong.
  • HTLC fulfill on the peer's commitment: this is the real risk. If the fulfill stays below the minimum until the HTLC expires, the peer can time it out, and we lose an amount we have already paid downstream.

Upstream Core Lightning has the same line (weight = bitcoin_tx_weight(tx) + 1 + 3 + 73 + 0 + tal_count(info->wscript); in lightningd/onchain_control.c), so this is worth sending there too.

How it was found

Running Lightning Fork (lnd) against this fork on a Bitcoin Knots BLAKE2b regtest, which has no fee estimates, so everything sits at the floor. lnd broadcast a revoked commitment with an HTLC on it. CLN's penalty for the HTLC was refused at every block, until lnd's HTLC-timeout confirmed and CLN penalised that instead. Decoding the refused transaction showed a witness of 71 + 33 + 136 bytes, 622 weight units against the roughly 590 the fee was computed for.

Tests

  • New test_penalty_htlc_min_feerate (anchors and not): test_penalty_inhtlc with every feerate at the floor. Without the change, the HTLC penalty is refused (min relay fee not met, 148 < 155). With it, both penalties are mined.
  • test_penalty_inhtlc, test_penalty_outhtlc, test_penalty_htlc_tx_fulfill and test_penalty_htlc_tx_timeout (both variants each) still pass.
  • test_coinmoves.py, as the review pointed out: three expected amounts now reflect the new fees (the fulfill, the HTLC penalty, and the to-local penalty, whose 0x01 element is now counted too). The commit is amended with them, and all 10 tests pass.
  • The penalty, onchain and HTLC tests in test_closing.py, and test_bookkeeper.py, pass with the fix (66 passed), except the two test_bookkeeper.py report tests that need currency rates from the network, which fail the same way without it.
  • In the lnd interop run of Add option_independent_secrets channel type #48, CLN now takes the HTLC with its first penalty.
  • Rebased onto blake2b-unified at 7974591 (v26.06.9-blake2b.7 plus the fork's latest fixes) without conflicts; the new test passes there, with and without anchors.

This is independent of option_independent_secrets and applies on its own to the fork's base. An independent review checked the arithmetic for every case (penalties on offered and received HTLCs, with and without anchors, and fulfills): all fall 7 to 8 sat short of the minimum at 1 sat/vbyte, and all clear it with the fix. Spends whose element is 0x01 were already covered by the slack in the 73-byte signature; they now count that byte too, so their fee rises by a few sat. Spends with no element are unaffected. We found no existing issue or PR about this upstream.

@kwsantiago kwsantiago left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The coinmoves tests hardcode the old fees: fulfill 94534000→94238000, penalty 92908000→92512000 and 49858187000→49858175000 msat; all pass then.

onchaind_tx_unsigned() estimates a spend's weight as the unsigned
transaction plus a worst-case signature and the witness script, but
leaves out the stack element between them: the revocation pubkey of a
penalty for an HTLC output (33 bytes), or the preimage of an HTLC
fulfill (32 bytes).  The fee then falls a few percent short of the
feerate it was meant to pay.  Usually that only costs a little time, but
when every estimate is at the floor (253 sat/kw, just above a minimum
relay fee of 1 sat/vbyte) the transaction pays less than the minimum
relay fee, and bitcoind refuses it each time it is rebroadcast:

    min relay fee not met, 148 < 155

For an HTLC penalty, the cheater's HTLC-timeout or HTLC-success
transaction can then confirm first, and we have to penalise its output
instead.  For a fulfill, the peer can time the HTLC out.

test_penalty_htlc_min_feerate is test_penalty_inhtlc with every feerate
at the floor: without this change its HTLC penalty never gets into the
mempool.

Changelog-Fixed: Onchain spends with a revocation pubkey or preimage in their witness (HTLC penalties and fulfills) no longer pay less than the minimum relay fee when feerate estimates are at the floor.
@davelund110
davelund110 force-pushed the onchain-witness-weight branch from db76cec to addaee3 Compare October 18, 2026 09:06
@davelund110

Copy link
Copy Markdown
Author

Thanks, updated with your values and folded into the commit (addaee3). All 10 coinmoves tests pass. The closing and bookkeeping tests pass too, except the two report tests that need live currency rates.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants