opening: reject fundchannel_complete with unsigned non-segwit inputs
What changed, and why it matters
This commit fixes a bug in Core Lightning's channel-funding command. If a user supplied an unsigned old-style (non-SegWit) bitcoin input when completing a channel open, the software would compute a transaction ID that could change later when the user actually signed and broadcast the transaction. The node would then wait forever for a funding transaction it would never recognize, leaving the channel opening process stuck. The fix rejects such inputs up front with a clear error message.
Apply the patch. It is a defensive correctness fix with low risk; no immediate incident response is indicated unless nodes are observed stuck waiting for non-existent funding transactions.
Security signals we found
Denial-of-service / stuck state: unresolvable funding wait due to malleable txid
Input-validation gap in RPC command
Non-SegWit transaction malleability
Regression test added for the rejected condition
Evidence from the diff
In json_fundchannel_complete(), before extracting a funding txid from the supplied PSBT, the code now iterates over all PSBT inputs and rejects any input that lacks a witness_utxo (meaning it is non-SegWit/legacy) and also lacks a finalized scriptsig (PSBT_IN_FINAL_SCRIPTSIG). For these inputs, the txid is malleable because the scriptsig is empty until signed, so psbt_txid() would return a txid that differs from the final broadcast transaction. A regression test creates a P2PKH input and verifies fundchannel_complete fails with ‘non-segwit and unsigned’.
Changed components
lightningd/opening_control.cfundchannel_complete RPC commandopeningd funding flowInspect captured patch +45 / −0
diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c
index 820bf4e..74a3277 100644
--- a/lightningd/opening_control.c
+++ b/lightningd/opening_control.c
@@ -1094,6 +1094,18 @@ static struct command_result *json_fundchannel_complete(struct command *cmd,
[*funding_txout_num].amount,
fmt_amount_sat(tmpctx, fc->funding_sats));
+ /* Unsigned non-segwit inputs have malleable txids. */
+ for (size_t i = 0; i < funding_psbt->num_inputs; i++) {
+ struct wally_psbt_input *in = &funding_psbt->inputs[i];
+ if (!in->witness_utxo
+ && !wally_map_get_integer(&in->psbt_fields,
+ /* PSBT_IN_FINAL_SCRIPTSIG */ 0x07))
+ return command_fail(cmd, FUNDING_PSBT_INVALID,
+ "Input %zu is non-segwit and unsigned:"
+ " txid is unknown until signed",
+ i);
+ }
+
funding_txid = tal(cmd, struct bitcoin_txid);
psbt_txid(NULL, funding_psbt, funding_txid, NULL);
diff --git a/tests/test_connection.py b/tests/test_connection.py
index 5520b32..6a0b38a 100644
--- a/tests/test_connection.py
+++ b/tests/test_connection.py
@@ -1779,6 +1779,39 @@ def test_funding_external_wallet(node_factory, bitcoind):
l3.rpc.close(l2.info["id"])
+@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need')
+@pytest.mark.openchannel('v1')
+def test_fundchannel_complete_rejects_nonsegwit_unsigned(node_factory, bitcoind):
+ l1, l2 = node_factory.get_nodes(2)
+ l1.connect(l2)
+
+ amount = 1_000_000
+
+ start = l1.rpc.fundchannel_start(l2.info['id'], amount)
+ funding_addr = start['funding_address']
+
+ # P2PKH (non-segwit) input.
+ legacy_addr = bitcoind.rpc.getnewaddress("", "legacy")
+ bitcoind.rpc.sendtoaddress(legacy_addr, 0.05)
+ bitcoind.generate_block(1)
+
+ utxos = bitcoind.rpc.listunspent(1, 9999999, [legacy_addr])
+ assert len(utxos) > 0
+ utxo = utxos[0]
+
+ psbt = bitcoind.rpc.walletcreatefundedpsbt(
+ [{"txid": utxo['txid'], "vout": utxo['vout']}],
+ [{funding_addr: amount / 10**8}],
+ 0,
+ {"add_inputs": False}
+ )['psbt']
+
+ with pytest.raises(RpcError, match=r'non-segwit and unsigned'):
+ l1.rpc.fundchannel_complete(l2.info['id'], psbt)
+
+ l1.rpc.fundchannel_cancel(l2.info['id'])
+
+
@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need')
@pytest.mark.openchannel('v1') # We manually turn on dual-funding for select nodes
def test_multifunding_v1_v2_mixed(node_factory, bitcoind):
Why this scored 49/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.