Skip to content

dualopend: verify the peer's input witnesses before the funding tx is broadcast - #51

Merged
kwsantiago merged 3 commits into
blake2b-unifiedfrom
dualopen-verify-peer-witnesses
Oct 19, 2026
Merged

kwsantiago merged 3 commits into
blake2b-unifiedfrom
dualopen-verify-peer-witnesses

Conversation

@kwsantiago

@kwsantiago kwsantiago commented Oct 18, 2026 •

Copy link
Copy Markdown

dualopend now checks the witness the peer sends for each of its funding inputs as its tx_signatures arrive, as #45 does for splices, and fails the channel if one is invalid. That covers the opener and the accepter, reconnection and RBF, which all receive them in handle_tx_sigs.

Until now nothing checked them: dualopend applied the peer's witnesses to the PSBT, and lightningd broadcast the funding transaction once both sides had signed. A peer that sends a bad witness for its own input, while we still have to sign, gets our signatures for a funding transaction bitcoind rejects (mempool-script-verify-flag-failed), and may hold a valid copy with the same txid to broadcast whenever it likes, or never. BOLT 2 also says the receiver of tx_signatures MUST fail the negotiation if the witnesses are non-standard, or a signature uses a hash type other than 0x01 (or 0x21 under option_unified_sigs), and neither was enforced on opens.

Which inputs are checked depends on whether we have sent our own tx_signatures:

  • Before we have: every input of the peer's must be a valid spend of a P2WPKH output, a P2TR output by its key path, or a P2WSH 2-of-2 funding output or anchor to_remote, as in channeld: verify the peer's input witnesses before signing a splice #45. Anything else fails the channel before we sign. An opener using fundchannel or multifundchannel is always in this case, since those wait for the accepter's tx_signatures before signing; a peer that withholds its signatures just stalls the open. openchannel_signed sends ours as soon as it is called, so a peer that waits for them gets the check below instead. CLN's accepter always signs first, so it is never in this case.
  • After we have: the peer holds our signatures already, so inputs we can't evaluate (other P2WSH scripts and P2TR script paths) are left to bitcoind as before, and an honest peer funding from a multisig wallet isn't force-closed. Everything else is still checked: a P2TR witness with an annex, and a spend of any other witness program (non-standard, but for P2A with an empty witness, which BOLT 2 doesn't allow either), fail.

Changes:

  • The check moves into common/psbt_internal.c: psbt_tx_with_spent_outputs() builds the transaction with each input's spent output attached, psbt_input_witness_known() says whether an input is one the verifier can judge, and psbt_their_witnesses_invalid() checks a side's inputs, skipping unknown ones unless strict. channeld calls it strict, so splices behave as before.
  • dualopend ignores a tx_signatures repeated after it has the peer's witnesses (lightningd keeps that flag with the PSBT holding them), and fails the negotiation when the peer sends more witnesses than it added inputs.
  • doc/blake2b-upgrade.md no longer says no peer verifies funding input signatures.

Tests, with EXPERIMENTAL_DUAL_FUND=1:

  • test_v2_open_bad_input_witness_from_accepter: the accepter (a test plugin, df_bad_witness.py) puts in an input and corrupts its signature. It signs first, so the opener fails the channel without sending its own tx_signatures, openchannel_signed then fails, and the funding transaction is never broadcast.
  • test_v2_open_bad_input_witness_from_opener: the opener corrupts its signature in the PSBT it passes to openchannel_signed. The accepter has signed already, so it fails the channel without broadcasting. The opener then broadcasts the valid copy, and the accepter recovers its funds through its commitment (ONCHAIN).
  • test_v2_open_unrecognized_input_script: the opener funds from a 1-of-1 multisig P2WSH output in bitcoind's wallet, signed by bitcoind. The accepter has signed already, so it lets the witness through, and the channel opens.
  • test_v2_open_unrecognized_input_before_we_sign: the accepter (a test plugin, df_external_input.py) funds from such an output and has bitcoind sign it. It signs first, so the opener fails the channel with P2WSH input N is not a 2-of-2 spend without sending its own tx_signatures.
  • With the check disabled, the two bad-witness tests fail, and the accepter in the second broadcasts the corrupted funding transaction itself.
  • run-psbt_input_witness: which inputs are known (including an annex, a P2TR script path, a P2WSH script that doesn't match its output, a v2 program and P2A), and strict versus non-strict checking by role and ignored index. Reversing the strict test, or treating an annex or other witness programs as unknown, makes it fail.

Locally, the splicing, unified, opening and connection suites pass without and with EXPERIMENTAL_DUAL_FUND (246 and 164 passed), the opening and splicing suites again after the last commit (40 and 28 passed with it), check-units, the source checks and flake8 pass, and the four dual-open tests above pass 12 of 12 runs.

Not in this PR:

  • The splice rule that refuses a peer input spending a coinbase output. The spec states it for splices only, so dualopend still accepts one.
  • openchannel_signed, openchannel_update and openchannel_bump crash lightningd on a version 0 PSBT, which openchannel_init converts. That is a separate fix.

Changelog-None

@kwsantiago
kwsantiago force-pushed the dualopen-verify-peer-witnesses branch from 2fca0df to bc07616 Compare October 19, 2026 00:33
@kwsantiago
kwsantiago merged commit 5d0641e into blake2b-unified Oct 19, 2026
46 checks passed
@kwsantiago
kwsantiago deleted the dualopen-verify-peer-witnesses branch October 19, 2026 05:32
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