pytest: test for watching utxos over restart reorg.
What changed, and why it matters
This commit only adds a new automated test to the project's test suite. It does not change any production code, so it cannot directly introduce or fix a security vulnerability in running software. The test is currently marked as expected to fail (xfail), meaning it documents behavior that is not yet working correctly—specifically, how the wallet tracks whether a coin is spent across a blockchain reorganization and a node restart. It may be a regression test for a future bug fix, but by itself it poses no security risk.
No immediate action is required for this commit. Treat it as test-suite maintenance. If the underlying behavior the test exercises is later fixed, ensure the xfail marker is removed or updated.
Security signals we found
No production code changes
Test-only commit
xfail marker indicates known failing behavior, not a resolved issue
Evidence from the diff
The diff adds a single pytest function, test_unspend_during_reorg, to tests/test_wallet.py. The test sets up a three-node Lightning network, mines blocks, records a channel funding outpoint as unspent, closes the channel so the outpoint becomes spent, restarts a watching node after additional blocks are mined, and asserts that the database still records the correct spendheight. It is decorated with @pytest.mark.xfail(strict=True), so it documents known-failing behavior. No wallet, gossip, or database implementation code is modified.
Changed components
tests/test_wallet.pyInspect captured patch +47 / −2
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index 0a2d7ec2..f4aeae21 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -4,9 +4,9 @@ from fixtures import * # noqa: F401,F403
from fixtures import TEST_NETWORK
from pyln.client import RpcError, Millisatoshi
from utils import (
- only_one, wait_for, sync_blockheight,
+ only_one, wait_for, sync_blockheight, mine_funding_to_announce,
VALGRIND, check_coin_moves, TailableProc, scriptpubkey_addr,
- check_utxos_channel, check_feerate, did_short_sig
+ check_utxos_channel, check_feerate, did_short_sig, first_scid,
)
import os
@@ -2536,3 +2536,48 @@ def test_hsm_wrong_passphrase_crash(node_factory):
os.close(master_fd2)
os.close(slave_fd2)
+
+
+@pytest.mark.xfail(strict=True)
+def test_unspend_during_reorg(node_factory, bitcoind):
+ l1, l2 = node_factory.line_graph(2)
+ scid = first_scid(l1, l2)
+ blockheight, txindex, _ = scid.split('x')
+
+ # Use mainnet settings for rescan.
+ l3 = node_factory.get_node(options={'rescan': 15})
+ l3.connect(l2)
+
+ mine_funding_to_announce(bitcoind, [l1, l2, l3])
+ bitcoind.generate_block(20)
+ sync_blockheight(bitcoind, [l3])
+ wait_for(lambda: len(l3.rpc.listchannels()['channels']) == 2)
+
+ # db shows it unspent.
+ assert only_one(l1.db_query(f"SELECT spendheight as spendheight FROM utxoset WHERE blockheight={blockheight} AND txindex={txindex}"))['spendheight'] is None
+
+ # Now, l3 sees the close, marks channel dying.
+ l1.rpc.close(l2.info['id'])
+ spentheight = bitcoind.rpc.getblockcount() + 1
+ bitcoind.generate_block(14, wait_for_mempool=1)
+ wait_for(lambda: len(l3.rpc.listchannels()['channels']) == 2)
+
+ # In one fell swoop it goes through dying, to dead (12 blocks)
+ l3.daemon.wait_for_log(f"Adding block {spentheight}")
+ l3.daemon.wait_for_log(f"gossipd: channel {scid} closing soon due to the funding outpoint being spent")
+ l3.daemon.wait_for_log(f"gossipd: Deleting channel {scid} due to the funding outpoint being spent")
+
+ # db shows it spent
+ assert only_one(l3.db_query(f"SELECT spendheight as spendheight FROM utxoset WHERE blockheight={blockheight} AND txindex={txindex}"))['spendheight'] == spentheight
+
+ # Restart, see replay.
+ l3.stop()
+ # This is enough to take channel from dying to dead.
+ bitcoind.generate_block(10)
+
+ l3.start()
+ # Channel should still be dead.
+ l3.daemon.wait_for_log(f"Adding block {spentheight}")
+
+ sync_blockheight(bitcoind, [l3])
+ assert only_one(l3.db_query(f"SELECT spendheight as spendheight FROM utxoset WHERE blockheight={blockheight} AND txindex={txindex}"))['spendheight'] == spentheight
Why this scored 12/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.