txprepare: fix withdraw returning unsigned transaction
What changed, and why it matters
This commit fixes a bug in Core Lightning's 'withdraw' command where the transaction shown to the user was returned without valid signatures or witness data, even though the actual network broadcast worked correctly. The returned 'tx' field looked like a real transaction but would be rejected if a user tried to broadcast it themselves. The fix ensures the returned transaction is fully signed and final.
Apply the patch. Users relying on the 'tx' field from withdraw for offline rebroadcast or external verification should upgrade, as previous versions may return an invalid/unbroadcastable transaction. No immediate network-level exploit is indicated.
Security signals we found
API response returned unsigned transaction data to the user
Regression introduced by libwally 0.8.8 / PSBTv2 update
Witness and scriptSig data stripped due to WALLY_PSBT_EXTRACT_NON_FINAL flag
Fix finalizes PSBT before extracting response transaction
Tests changed from expected-fail to expected-pass
Evidence from the diff
In plugins/txprepare.c, signpsbt_done() previously called psbt_txid() with a non-final PSBT extraction, which uses wally_psbt_extract(WALLY_PSBT_EXTRACT_NON_FINAL) and strips all signature/witness data. As a result, utx->tx contained an unsigned transaction that was returned in the withdraw response’s ‘tx’ field. The actual sendpsbt path finalized internally, so broadcast succeeded. The patch finalizes the PSBT with psbt_finalize() and extracts the final signed transaction via psbt_final_tx() for the response, while still using psbt_txid() only for txid verification. Tests that expected failure (xfail) are now enabled.
Changed components
plugins/txprepare.cwithdraw RPC commandsignpsbt_done() functionPSBT finalization and transaction extraction logicInspect captured patch +17 / −7
diff --git a/plugins/txprepare.c b/plugins/txprepare.c
index 5d97b7e..e69b0ba 100644
--- a/plugins/txprepare.c
+++ b/plugins/txprepare.c
@@ -156,16 +156,28 @@ static struct command_result *signpsbt_done(struct command *cmd,
/* Replace with signed tx. */
tal_free(utx->tx);
- /* The txid from the final should match our expectation. */
- psbt_txid(utx, utx->psbt, &txid, &utx->tx);
+ /* The txid from the signed PSBT should match our expectation. */
+ psbt_txid(NULL, utx->psbt, &txid, NULL);
if (!bitcoin_txid_eq(&txid, &utx->txid)) {
return command_fail(cmd, LIGHTNINGD,
"Signed tx changed txid? Had '%s' now '%s'",
- tal_hex(tmpctx,
- linearize_wtx(tmpctx, utx->tx)),
- fmt_wally_psbt(tmpctx, utx->psbt));
+ fmt_bitcoin_txid(tmpctx, &utx->txid),
+ fmt_bitcoin_txid(tmpctx, &txid));
}
+ /* Finalize the signed PSBT and extract the fully signed tx,
+ * so that utx->tx contains witness data for the response. */
+ if (!psbt_finalize(utx->psbt))
+ return command_fail(cmd, LIGHTNINGD,
+ "Signed PSBT not finalizeable: %s",
+ fmt_wally_psbt(tmpctx, utx->psbt));
+
+ utx->tx = psbt_final_tx(utx, utx->psbt);
+ if (!utx->tx)
+ return command_fail(cmd, LIGHTNINGD,
+ "Could not extract final tx: %s",
+ fmt_wally_psbt(tmpctx, utx->psbt));
+
req = jsonrpc_request_start(cmd, "sendpsbt",
sendpsbt_done, forward_error,
utx);
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index 66c93f6..31f28b2 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -2072,7 +2072,6 @@ def test_fundchannel_listtransaction(node_factory, bitcoind):
assert tx['blockheight'] == 0
-@pytest.mark.xfail(strict=True)
@unittest.skipIf(TEST_NETWORK != 'regtest', "Uss p2tr")
def test_withdraw_returns_signed_tx(node_factory, bitcoind):
"""
@@ -2109,7 +2108,6 @@ def test_withdraw_returns_signed_tx(node_factory, bitcoind):
assert decoded['txid'] == out['txid']
-@pytest.mark.xfail(strict=True)
@unittest.skipIf(TEST_NETWORK != 'regtest', "Uss p2tr")
def test_withdraw_close_output_signed(node_factory, bitcoind):
"""
Why this scored 33/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.