diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index d203bd60bae0..1450ebc6a495 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -897,6 +897,12 @@ void channel_apply_scid(struct channel *channel, if (channel->scid && short_channel_id_eq(*channel->scid, scid)) return; + /* Gossip already has a closing channel dying under its scid: a + * candidate adopted while closing (a splice mined in place of the + * mutual close) doesn't change that. */ + if (channel->scid && channel_state_closing(channel->state)) + return; + if (!channel->scid) { wallet_annotate_txout(ld->wallet, outpoint, TX_CHANNEL_FUNDING, channel->dbid); diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index 464697056de9..6dc019de660e 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -357,6 +357,10 @@ static enum watch_result funding_spent(struct channel *channel, const struct bitcoin_tx *tx, size_t inputnum UNUSED, const struct block *block); +static void adopt_opening_candidate(struct channel *channel, + const struct channel_inflight *inflight); +static void adopt_splice_candidate(struct channel *channel, + const struct channel_inflight *inflight); /* We coop-closed channel: if another inflight confirms, force close */ static void closed_inflight_splice_found(struct lightningd *ld, @@ -365,12 +369,27 @@ static void closed_inflight_splice_found(struct lightningd *ld, const struct txlocator *loc, struct channel_inflight *inflight) { - /* This is now the main tx. */ - update_channel_from_inflight(ld, inflight->channel, inflight, false); - channel_fail_saw_onchain(inflight->channel, - REASON_UNKNOWN, - tx, - "Inflight tx confirmed after mutual close"); + struct channel *channel = inflight->channel; + struct bitcoin_txid txid; + + /* Already adopted (a reorg mined it again). */ + if (bitcoin_outpoint_eq(&channel->funding, &inflight->funding->outpoint)) + return; + + /* This is now the main tx. It only moved the funds to a new + * funding output, so unlike a commitment it closed nothing: + * publish ours on it. */ + if (channel->scid) + adopt_splice_candidate(channel, inflight); + else + adopt_opening_candidate(channel, inflight); + wallet_channel_save(ld->wallet, channel); + bitcoin_txid(tx, &txid); + channel_fail_permanent(channel, + REASON_UNKNOWN, + "Inflight tx confirmed after mutual close:" + " onchain txid %s", + fmt_bitcoin_txid(tmpctx, &txid)); } void drop_to_chain(struct lightningd *ld, struct channel *channel, diff --git a/tests/test_opening.py b/tests/test_opening.py index 3b7a5e2bb60e..fc08aeee1d6e 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -4536,3 +4536,46 @@ def test_rbf_mutual_close_before_lockin(node_factory, bitcoind): # Startup resends the close, which takes the same path. l2.restart() assert only_one(l2.rpc.listpeerchannels()['channels'])['state'] == 'CLOSINGD_COMPLETE' + + +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +@pytest.mark.openchannel('v2') +def test_rbf_candidate_confirms_after_mutual_close(node_factory, bitcoind): + """An earlier dual-funding attempt mined in place of the mutual close + leaves the funds in its output, so we must close that with a + commitment.""" + l1, l2 = node_factory.get_nodes(2) + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + chan_amount = 100000 + l1.fundwallet(2**24) + + res = l1.rpc.fundchannel(l2.info['id'], chan_amount) + chan_id = res['channel_id'] + first_txid = res['txid'] + vin = only_one(bitcoind.rpc.decoderawtransaction(res['tx'])['vin']) + l1.daemon.wait_for_log(' to DUALOPEND_AWAITING_LOCKIN') + + initpsbt = l1.rpc.utxopsbt(chan_amount, find_next_feerate(l1, l2), 42 + 172, + ["{}:{}".format(vin['txid'], vin['vout'])], + reservedok=True, excess_as_change=True)['psbt'] + bump = l1.rpc.openchannel_bump(chan_id, chan_amount, initpsbt) + update = l1.rpc.openchannel_update(chan_id, bump['psbt']) + assert update['commitments_secured'] + l1.rpc.openchannel_signed(chan_id, l1.rpc.signpsbt(update['psbt'])['signed_psbt']) + wait_for(lambda: len(only_one(l1.rpc.listpeerchannels()['channels'])['inflight']) == 2) + + l1.rpc.close(chan_id) + for n in (l1, l2): + wait_for(lambda: only_one(n.rpc.listpeerchannels()['channels'])['state'] == 'CLOSINGD_COMPLETE') + + # The first attempt is mined, not the bump the close spends. + bitcoind.rpc.generateblock(bitcoind.rpc.getnewaddress(), [res['tx']]) + for n in (l1, l2): + n.daemon.wait_for_log('Inflight tx confirmed after mutual close') + chan = only_one(l1.rpc.listpeerchannels()['channels']) + assert chan['funding_txid'] == first_txid + assert chan['to_us_msat'] == chan_amount * 1000 + + bitcoind.generate_block(1, wait_for_mempool=1) + for n in (l1, l2): + n.daemon.wait_for_log(r'Resolved FUNDING_TRANSACTION/FUNDING_OUTPUT by (OUR|THEIR)_UNILATERAL') diff --git a/tests/test_splicing.py b/tests/test_splicing.py index 785e80b814c8..03e3970948f7 100644 --- a/tests/test_splicing.py +++ b/tests/test_splicing.py @@ -2,6 +2,7 @@ from pyln.client import LightningRpc, RpcError from pyln.testing.utils import EXPERIMENTAL_DUAL_FUND, SLOW_MACHINE, VALGRIND import os +import re import pytest import threading import unittest @@ -1376,6 +1377,61 @@ def test_splice_restart_watches_funding_spend(node_factory, bitcoind): 'THEIR_REVOKED_UNILATERAL/DELAYED_CHEAT_OUTPUT_TO_THEM')) +@pytest.mark.openchannel('v1') +@pytest.mark.openchannel('v2') +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +def test_splice_confirms_after_mutual_close(node_factory, bitcoind): + """A splice mined in place of the mutual close leaves the funds in its + output, so we must close that with a commitment.""" + l1, l2 = node_factory.line_graph(2, fundamount=1000000, wait_for_announce=True) + chan_id = l1.get_channel_id(l2) + + funds_result = l1.rpc.fundpsbt("111722sat", 0, 0, excess_as_change=True) + result = l1.rpc.splice_init(chan_id, 100000, funds_result['psbt']) + result = l1.rpc.splice_update(chan_id, result['psbt']) + result = l1.rpc.splice_update(chan_id, result['psbt']) + assert result['commitments_secured'] is True + result = l1.rpc.signpsbt(result['psbt']) + splice_txid = l1.rpc.splice_signed(chan_id, result['signed_psbt'])['txid'] + for n in (l1, l2): + n.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') + wait_for(lambda: bitcoind.rpc.getrawmempool() == [splice_txid]) + + # The mutual close spends the old funding, so it loses to the splice. + close_txid = only_one(l1.rpc.close(chan_id)['txids']) + for n in (l1, l2): + wait_for(lambda: only_one(n.rpc.listpeerchannels()['channels'])['state'] == 'CLOSINGD_COMPLETE') + assert bitcoind.rpc.getrawmempool() == [splice_txid] + + bitcoind.generate_block(1) + for n in (l1, l2): + n.daemon.wait_for_log('Inflight tx confirmed after mutual close') + line = l1.daemon.wait_for_log(r'Broadcasting txid [0-9a-f]{64}') + commit_txid = re.search(r'Broadcasting txid ([0-9a-f]{64})', line).group(1) + assert commit_txid != close_txid + chan = only_one(l1.rpc.listpeerchannels()['channels']) + assert chan['funding_txid'] == splice_txid + assert chan['to_us_msat'] == 1100000000 + splice_out = chan['funding_outnum'] + + # Restart within the rescan window, then past it, before any + # commitment confirms. + l1.restart() + assert only_one(l1.rpc.listpeerchannels()['channels'])['funding_txid'] == splice_txid + l1.stop() + for _ in range(20): + bitcoind.rpc.generateblock(bitcoind.rpc.getnewaddress(), []) + l1.start() + l1.daemon.wait_for_log('Broadcasting txid {}'.format(commit_txid)) + chan = only_one(l1.rpc.listpeerchannels()['channels']) + assert chan['funding_txid'] == splice_txid + assert chan['to_us_msat'] == 1100000000 + bitcoind.generate_block(1, wait_for_mempool=1) + for n in (l1, l2): + n.daemon.wait_for_log(r'Resolved FUNDING_TRANSACTION/FUNDING_OUTPUT by (OUR|THEIR)_UNILATERAL') + assert bitcoind.rpc.gettxout(splice_txid, splice_out) is None + + @pytest.mark.openchannel('v1') @pytest.mark.openchannel('v2') @unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need')