tests: add regression tests for withdraw returning unsigned tx
What changed, and why it matters
This commit only adds two new automated tests that demonstrate an existing bug: the 'withdraw' command returns an unsigned raw transaction in its 'tx' field because an internal helper strips away signatures. The tests are marked as expected-to-fail, so the actual code flaw is not fixed here. A user relying on the returned transaction could receive a version that looks valid but would be rejected by the Bitcoin network because it lacks required witness data. The commit documents the regression but does not change the wallet logic itself.
Treat this as a test-only commit that reproduces a known bug. The actual fix must be made in the wallet/PSBT extraction logic so withdraw returns a finalized, signed transaction (or the correct signed raw tx) instead of the non-final extraction. Review and merge the separate production-code fix, then remove the xfail markers from these tests to confirm the bug is resolved.
Security signals we found
Regression tests for issue #8701: withdraw returns unsigned raw transaction
Root cause identified in commit message: psbt_txid() uses WALLY_PSBT_EXTRACT_NON_FINAL which strips signatures/witnesses
Tests assert all segwit inputs have non-empty witness data in withdraw response
Tests cover both regular wallet UTXOs and channel close outputs (anchor/P2WSH with CSV locks)
Tests marked xfail(strict=True): bug is reproduced but not fixed by this commit
Evidence from the diff
The diff adds two pytest regression tests in tests/test_wallet.py for GitHub issue #8701. They call rpc.withdraw(), decode the returned out[‘tx’], and assert every input has a non-empty txinwitness and that decoded txid matches out[‘txid’]. Both tests are decorated with @pytest.mark.xfail(strict=True), meaning they document the bug and will fail until the underlying code is fixed. The commit message states the root cause is psbt_txid() using WALLY_PSBT_EXTRACT_NON_FINAL, which extracts an unsigned transaction, and withdraw returning that instead of the finalized PSBT/transaction. No production code is patched, so this commit alone does not remediate the issue.
Changed components
tests/test_wallet.pywithdraw RPC commandpsbt_txid() helperwallet transaction signing / PSBT finalization flowInspect captured patch +75 / −0
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index 0eb0433..66c93f6 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -2072,6 +2072,81 @@ 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):
+ """
+ Test that withdraw returns a fully signed transaction in the 'tx' field.
+
+ Regression test for https://github.com/ElementsProject/lightning/issues/8701
+ where withdraw returned an unsigned transaction (empty witnesses) because
+ psbt_txid() used WALLY_PSBT_EXTRACT_NON_FINAL to extract the tx.
+ """
+ l1 = node_factory.get_node(random_hsm=True)
+
+ # Fund the wallet with a few UTXOs
+ addr = l1.rpc.newaddr('p2tr')['p2tr']
+ for i in range(3):
+ l1.bitcoin.rpc.sendtoaddress(addr, 0.01)
+ bitcoind.generate_block(1)
+ wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 3)
+
+ waddr = l1.bitcoin.rpc.getnewaddress()
+ out = l1.rpc.withdraw(waddr, 'all')
+
+ # The tx field must be a fully signed transaction
+ decoded = bitcoind.rpc.decoderawtransaction(out['tx'])
+
+ # Every segwit input must have witness data (txinwitness)
+ for i, vin in enumerate(decoded['vin']):
+ assert 'txinwitness' in vin, \
+ f"Input {i} has no witness data - tx is unsigned! (issue #8701)"
+ assert len(vin['txinwitness']) > 0, \
+ f"Input {i} has empty witness stack"
+
+ # The returned tx must be directly broadcastable (already sent by withdraw,
+ # but verify it could be re-sent by checking it was accepted)
+ 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):
+ """
+ Test that withdraw correctly signs close outputs (anchor/P2WSH).
+
+ Regression test for https://github.com/ElementsProject/lightning/issues/8701
+ The original issue involved spending channel close outputs (with
+ option_anchors CSV=1) alongside regular wallet UTXOs.
+ """
+ l1, l2 = node_factory.line_graph(2, fundchannel=True, wait_for_announce=True)
+
+ # Close the channel so l1 gets a close output
+ l1.rpc.close(l2.info['id'])
+ bitcoind.generate_block(1, wait_for_mempool=1)
+
+ # Wait for CSV lock (1 block for anchors) and the close output to mature
+ bitcoind.generate_block(100)
+ sync_blockheight(bitcoind, [l1])
+
+ wait_for(lambda: all(o['status'] == 'confirmed' for o in l1.rpc.listfunds()['outputs']))
+
+ # Withdraw all funds - this spends both regular and close outputs
+ waddr = l1.bitcoin.rpc.getnewaddress()
+ out = l1.rpc.withdraw(waddr, 'all')
+
+ decoded = bitcoind.rpc.decoderawtransaction(out['tx'])
+
+ # Every input must have witness data
+ for i, vin in enumerate(decoded['vin']):
+ assert 'txinwitness' in vin, \
+ f"Input {i} has no witness data - tx is unsigned! (issue #8701)"
+ assert len(vin['txinwitness']) > 0, \
+ f"Input {i} has empty witness stack"
+
+ assert decoded['txid'] == out['txid']
+
+
def test_withdraw_nlocktime(node_factory):
"""
Test that we don't set the nLockTime to 0 for withdrawal and
Why this scored 46/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.