Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lightningd/channel_control.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
31 changes: 25 additions & 6 deletions lightningd/peer_control.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions tests/test_opening.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
56 changes: 56 additions & 0 deletions tests/test_splicing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down
Loading