From a82bfa83ffa6dd7fcc06d805f59437e615d431dc Mon Sep 17 00:00:00 2001 From: niftynei Date: Thu, 17 Dec 2020 15:31:09 -0600 Subject: [PATCH] df-tests: have the df_accepter plugin keep track of attempts Test connection/reconnection handling for v2 opens. We needed to fixup the accepter plugin so that we were freeing up inputs on disconnect/failure. --- tests/plugins/df_accepter.py | 52 ++++++++++++++++++++++++++++++++- tests/test_connection.py | 56 ++++++++++++++++++++++++++++++++++-- 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/tests/plugins/df_accepter.py b/tests/plugins/df_accepter.py index 2c117b172..928eccf13 100755 --- a/tests/plugins/df_accepter.py +++ b/tests/plugins/df_accepter.py @@ -89,6 +89,7 @@ def find_inputs(b64_psbt): def init(configuration, options, plugin): # this is the max channel size, pre-wumbo plugin.max_fund = Millisatoshi((2 ** 24 - 1) * 1000) + plugin.inflight = {} plugin.log('max funding set to {}'.format(plugin.max_fund)) @@ -99,6 +100,35 @@ def set_accept_funding_max(plugin, max_sats, **kwargs): return {'accepter_max_funding': plugin.max_fund} +def add_inflight(plugin, peerid, chanid, psbt): + if peerid in plugin.inflight: + chans = plugin.inflight[peerid] + else: + chans = {} + plugin.inflight[peerid] = chans + + if chanid in chans: + raise ValueError("channel {} already in flight (peer {})".format(chanid, peerid)) + chans[chanid] = psbt + + +def cleanup_inflight(plugin, chanid): + for peer, chans in plugin.inflight.items(): + if chanid in chans: + psbt = chans[chanid] + del chans[chanid] + return psbt + return None + + +def cleanup_inflight_peer(plugin, peerid): + if peerid in plugin.inflight: + chans = plugin.inflight[peerid] + for chanid, psbt in chans.items(): + plugin.rpc.unreserveinputs(psbt) + del plugin.inflight[peerid] + + @plugin.hook('openchannel2') def on_openchannel(openchannel2, plugin, **kwargs): # We mirror what the peer does, wrt to funding amount ... @@ -144,8 +174,12 @@ def on_openchannel(openchannel2, plugin, **kwargs): output = tx_output_init(change.to_whole_satoshi(), get_script(addr)) psbt_add_output_at(psbt_obj, 0, 0, output) + psbt = psbt_to_base64(psbt_obj, 0) + add_inflight(plugin, openchannel2['id'], + openchannel2['channel_id'], psbt) plugin.log("contributing {} at feerate {}".format(amount, feerate)) - return {'result': 'continue', 'psbt': psbt_to_base64(psbt_obj, 0), + + return {'result': 'continue', 'psbt': psbt, 'accepter_funding_msat': amount, 'funding_feerate': feerate} @@ -168,7 +202,23 @@ def on_tx_sign(openchannel2_sign, plugin, **kwargs): else: final_psbt = psbt + cleanup_inflight(plugin, openchannel2_sign['channel_id']) return {'result': 'continue', 'psbt': final_psbt} +@plugin.subscribe("channel_open_failed") +def on_open_failed(channel_open_failed, plugin, **kwargs): + channel_id = channel_open_failed['channel_id'] + psbt = cleanup_inflight(plugin, channel_id) + if psbt: + plugin.log("failed to open channel {}, unreserving".format(channel_id)) + plugin.rpc.unreserveinputs(psbt) + + +@plugin.subscribe("disconnect") +def on_peer_disconnect(id, plugin, **kwargs): + plugin.log("peer {} disconnected, removing inflights".format(id)) + cleanup_inflight_peer(plugin, id) + + plugin.run() diff --git a/tests/test_connection.py b/tests/test_connection.py index a5340f603..f1cd75b2f 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -308,6 +308,14 @@ def test_disconnect_fundee(node_factory): disconnects = ['-WIRE_ACCEPT_CHANNEL', '@WIRE_ACCEPT_CHANNEL', '+WIRE_ACCEPT_CHANNEL'] + if EXPERIMENTAL_DUAL_FUND: + disconnects = ['-WIRE_ACCEPT_CHANNEL2', + '@WIRE_ACCEPT_CHANNEL2', + '+WIRE_ACCEPT_CHANNEL2', + '-WIRE_TX_COMPLETE', + '@WIRE_TX_COMPLETE', + '+WIRE_TX_COMPLETE'] + l1 = node_factory.get_node() l2 = node_factory.get_node(disconnect=disconnects) @@ -328,6 +336,47 @@ def test_disconnect_fundee(node_factory): assert len(l2.rpc.listpeers()) == 1 +@unittest.skipIf(not DEVELOPER, "needs DEVELOPER=1") +@unittest.skipIf(not EXPERIMENTAL_DUAL_FUND, "needs OPT_DUAL_FUND") +def test_disconnect_fundee_v2(node_factory): + # Now error on fundee side during channel open, with them funding + disconnects = ['-WIRE_ACCEPT_CHANNEL2', + '@WIRE_ACCEPT_CHANNEL2', + '+WIRE_ACCEPT_CHANNEL2', + '-WIRE_TX_ADD_INPUT', + '@WIRE_TX_ADD_INPUT', + '+WIRE_TX_ADD_INPUT', + '-WIRE_TX_ADD_OUTPUT', + '@WIRE_TX_ADD_OUTPUT', + '+WIRE_TX_ADD_OUTPUT', + '-WIRE_TX_COMPLETE', + '@WIRE_TX_COMPLETE', + '+WIRE_TX_COMPLETE'] + + accepter_plugin = os.path.join(os.path.dirname(__file__), + 'plugins/df_accepter.py') + l1 = node_factory.get_node() + l2 = node_factory.get_node(disconnect=disconnects, + options={'plugin': accepter_plugin}) + + l1.fundwallet(2000000) + l2.fundwallet(2000000) + + for d in disconnects: + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + with pytest.raises(RpcError): + l1.rpc.fundchannel(l2.info['id'], 25000) + assert l1.rpc.getpeer(l2.info['id']) is None + + # This one will succeed. + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + l1.rpc.fundchannel(l2.info['id'], 25000) + + # Should still only have one peer! + assert len(l1.rpc.listpeers()) == 1 + assert len(l2.rpc.listpeers()) == 1 + + @unittest.skipIf(not DEVELOPER, "needs DEVELOPER=1") def test_disconnect_half_signed(node_factory): # Now, these are the corner cases. Fundee sends funding_signed, @@ -2527,12 +2576,15 @@ def test_restart_many_payments(node_factory, bitcoind): @unittest.skipIf(not DEVELOPER, "need dev-disconnect") -@unittest.skipIf(True, "FIXME: doesn't work for opt_dual_fund, see test below") def test_fail_unconfirmed(node_factory, bitcoind, executor): """Test that if we crash with an unconfirmed connection to a known peer, we don't have a dangling peer in db""" + if EXPERIMENTAL_DUAL_FUND: + disconnect = ['=WIRE_OPEN_CHANNEL2'] + else: + disconnect = ['=WIRE_OPEN_CHANNEL'] # = is a NOOP disconnect, but sets up file. - l1 = node_factory.get_node(disconnect=['=WIRE_OPEN_CHANNEL']) + l1 = node_factory.get_node(disconnect=disconnect) l2 = node_factory.get_node() # First one, we close by mutual agreement.