Repository navigation
lightningd: count the witness stack element when sizing onchain spends - #49
Open
davelund110 wants to merge 1 commit into
Open
davelund110 wants to merge 1 commit into
davelund110 wants to merge 1 commit into
Conversation
kwsantiago
reviewed
Oct 18, 2026
kwsantiago
left a comment
There was a problem hiding this comment.
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
force-pushed
the
onchain-witness-weight
branch
from
October 18, 2026 09:06
db76cec to
addaee3
Compare
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 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:
feerate_for_target()usesget_feerate_floor()by design, whatever the estimates say. An HTLC penalty's deadline isto_self_delay(144 by default) after the close, so its first ~44 broadcasts always fall in this window;mempoolminfee(the error is then "mempool min fee not met").On our regtest:
Upstream Core Lightning has the same line (
weight = bitcoin_tx_weight(tx) + 1 + 3 + 73 + 0 + tal_count(info->wscript);inlightningd/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
test_penalty_htlc_min_feerate(anchors and not):test_penalty_inhtlcwith 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_fulfillandtest_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, whose0x01element is now counted too). The commit is amended with them, and all 10 tests pass.test_closing.py, andtest_bookkeeper.py, pass with the fix (66 passed), except the twotest_bookkeeper.pyreport tests that need currency rates from the network, which fail the same way without it.blake2b-unifiedat 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_secretsand 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 is0x01were 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.