diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index 79cae4bb1..a92f07f06 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -847,13 +847,14 @@ class LightningRpc(UnixDomainSocketRpc): } return self.call("fundchannel_cancel", payload) - def fundchannel_complete(self, node_id, psbt): + def fundchannel_complete(self, node_id, psbt, withhold=True): """ Complete channel establishment with {id}, using {psbt}. """ payload = { "id": node_id, "psbt": psbt, + "withhold": withhold, } return self.call("fundchannel_complete", payload) diff --git a/tests/test_opening.py b/tests/test_opening.py index 19e1a2724..2261bcd14 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -2867,3 +2867,79 @@ def test_sendpsbt_crash(bitcoind, node_factory): bitcoind.generate_block(1, wait_for_mempool=1) assert l1.daemon.is_in_log('Signed and sent psbt for waiting channel') + + +@pytest.mark.parametrize("stay_withheld", [True, False]) +@pytest.mark.parametrize("mutual_close", [True, False]) +def test_zeroconf_withhold(node_factory, bitcoind, stay_withheld, mutual_close): + plugin_path = Path(__file__).parent / "plugins" / "zeroconf-selective.py" + + l1, l2 = node_factory.get_nodes(2, opts=[{'may_reconnect': True, + 'dev-no-reconnect': None, + }, + {'plugin': str(plugin_path), + 'zeroconf_allow': '0266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c03518', + 'may_reconnect': True, + 'dev-no-reconnect': None, + }]) + # Try to open a mindepth=0 channel + l1.fundwallet(10**7) + + l1.connect(l2) + amount = 1000000 + funding_addr = l1.rpc.fundchannel_start(l2.info['id'], f"{amount}sat", mindepth=0)['funding_address'] + + # Create the funding transaction + psbt = l1.rpc.fundpsbt(amount, "1000perkw", 1000, excess_as_change=True)['psbt'] + psbt = l1.rpc.addpsbtoutput(1000000, psbt, destination=funding_addr)['psbt'] + + # Be sure fundchannel_complete is successful + assert l1.rpc.fundchannel_complete(l2.info['id'], psbt, withhold=True)['commitments_secured'] + + # It's withheld. + assert only_one(l1.rpc.listpeerchannels()['channels'])['funding']['withheld'] is True + + # We can use the channel (once they send an update) + wait_for(lambda: 'remote' in only_one(l1.rpc.listpeerchannels()['channels'])['updates']) + l1.rpc.xpay(l2.rpc.invoice(100, "test_zeroconf_withhold", "test_zeroconf_withhold")['bolt11']) + + # But mempool is empty! No funding tx! + assert bitcoind.rpc.getrawmempool() == [] + + # Restarting doesn't make it transmit! + l1.restart() + assert bitcoind.rpc.getrawmempool() == [] + + if mutual_close: + l1.connect(l2) + + if not stay_withheld: + # sendpsbt marks it as no longer withheld. + l1.rpc.sendpsbt(l1.rpc.signpsbt(psbt)['signed_psbt']) + assert only_one(l1.rpc.listpeerchannels()['channels'])['funding']['withheld'] is False + assert l1.daemon.is_in_log(r'Funding PSBT sent, and stored for rexmit \(was withheld\)') + wait_for(lambda: len(bitcoind.rpc.getrawmempool()) == 1) + + if mutual_close: + ret = l1.rpc.close(l2.info['id']) + else: + ret = l1.rpc.close(l2.info['id'], unilateraltimeout=1) + + if stay_withheld: + assert ret['txs'] == [] + assert ret['txids'] == [] + assert bitcoind.rpc.getrawmempool() == [] + else: + assert len(ret['txs']) == 1 + assert len(ret['txids']) == 1 + wait_for(lambda: len(bitcoind.rpc.getrawmempool()) == 2) + + # If withheld, it's moved to closed immediately. + if stay_withheld: + assert l1.rpc.listpeerchannels()['channels'] == [] + assert only_one(l1.rpc.listclosedchannels()['closedchannels'])['funding_withheld'] is True + else: + if mutual_close: + wait_for(lambda: only_one(l1.rpc.listpeerchannels()['channels'])['state'] == 'CLOSINGD_COMPLETE') + else: + wait_for(lambda: only_one(l1.rpc.listpeerchannels()['channels'])['state'] == 'AWAITING_UNILATERAL')