Skip to content

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

Open
davelund110 wants to merge 1 commit into
ElementsProject:masterfrom
davelund110:fix-onchain-witness-weight
Open

davelund110 wants to merge 1 commit into
ElementsProject:masterfrom
davelund110:fix-onchain-witness-weight

Conversation

@davelund110

Copy link
Copy Markdown

What

onchaind_tx_unsigned() sizes an onchain spend before signing it, to pick its fee. The estimate covers the unsigned transaction, a worst-case signature and the witness script, but not the stack element 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 adds the element's length to the estimate: 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 our signatures are low-R), about 5%. Whenever the feerate chosen is the floor, that puts the transaction below the minimum relay fee, and bitcoind refuses it every time it is rebroadcast:

min relay fee not met, 148 < 155

That happens when estimates sit at the floor, and also by design when the deadline is 101 to 200 blocks away, where feerate_for_target() uses get_feerate_floor() whatever the estimates say: an HTLC penalty's deadline is to_self_delay (144 by default) after the close, so its first ~44 broadcasts fall in that window. With a full mempool, the same shortfall can also fall under mempoolminfee.

  • HTLC penalty: the cheater's HTLC-timeout or HTLC-success transaction can confirm first, and we then have to penalise its output instead.
  • HTLC fulfill on the peer's commitment: if it stays below the minimum until the HTLC expires, the peer can time it out, and we lose an amount we've already paid downstream.

This is probably what #2530 ran into in 2019: an HTLC fulfill refused with min relay fee not met, 136 < 143, about 5% short, closed this year as stale without a cause found. The same line was in onchaind/onchaind.c then, and moved to lightningd/onchain_control.c in 2023.

How it was found

Testing penalties against lnd on a regtest with no fee estimates, so every feerate sat at the floor. CLN's penalty for an HTLC output was refused at every block until the cheater's HTLC-timeout confirmed and CLN penalised that instead. The refused transaction had a witness of 71 + 33 + 136 bytes, 622 weight units against the roughly 590 its fee was computed for.

Tests

  • New test_penalty_htlc_min_feerate (with and without anchors): 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_coinmoves.py: three expected amounts change with the new fees (the fulfill, the HTLC penalty, and the to-local penalty, whose 0x01 element is now counted too).
  • On master at 1ebc494, with the change: check-units; the new test, all of test_coinmoves.py, and the penalty, onchain and HTLC tests in test_closing.py (58 passed, 1 skipped); and test_bookkeeper.py, apart from the two report tests that need the Rust currencyrate plugin, which wasn't built here.

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.

The same fix is merged in a downstream fork, privkeyio#49.

Checklist

  • The changelog has been updated in the relevant commit(s) according to the guidelines.
  • Tests have been added or modified to reflect the changes.
  • Documentation has been reviewed and updated as needed (no documentation change needed).
  • Related issues have been listed and linked, including any that this PR closes.
  • All PRs must consider how to reverse any persistent changes for tools/lightning-downgrade (no persistent changes).

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.

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.

1 participant