diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index e87deaa50..b6aa40317 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -577,6 +577,21 @@ the string `reject` or `continue`. If `reject` and there's a member `error_message`, that member is sent to the peer before disconnection. +For a 'continue'd result, you can also include a `close_to` address, +which will be used as the output address for a mutual close transaction. + +e.g. + +```json +{ + "result": "continue", + "close_to": "bc1qlq8srqnz64wgklmqvurv7qnr4rvtq2u96hhfg2" +} +``` + +Note that `close_to` must be a valid address for the current chain; an invalid address will cause the node to exit with an error. + + #### `htlc_accepted` The `htlc_accepted` hook is called whenever an incoming HTLC is accepted, and diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c index f9b5c9d25..b7b6fca92 100644 --- a/lightningd/opening_control.c +++ b/lightningd/opening_control.c @@ -455,7 +455,7 @@ static void opening_fundee_finished(struct subd *openingd, u32 feerate; u8 channel_flags; struct channel *channel; - u8 *remote_upfront_shutdown_script; + u8 *remote_upfront_shutdown_script, *local_upfront_shutdown_script; struct per_peer_state *pps; log_debug(uc->log, "Got opening_fundee_finish_response"); @@ -482,6 +482,7 @@ static void opening_fundee_finished(struct subd *openingd, &feerate, &funding_signed, &uc->our_config.channel_reserve, + &local_upfront_shutdown_script, &remote_upfront_shutdown_script)) { log_broken(uc->log, "bad OPENING_FUNDEE_REPLY %s", tal_hex(reply, reply)); @@ -510,7 +511,7 @@ static void opening_fundee_finished(struct subd *openingd, channel_flags, &channel_info, feerate, - NULL, + local_upfront_shutdown_script, remote_upfront_shutdown_script); if (!channel) { uncommitted_channel_disconnect(uc, "Commit channel failed"); @@ -753,6 +754,7 @@ static void openchannel_hook_cb(struct openchannel_hook_payload *payload, const jsmntok_t *toks) { struct subd *openingd = payload->openingd; + const u8 *our_upfront_shutdown_script; const char *errmsg = NULL; /* We want to free this, whatever happens. */ @@ -782,14 +784,41 @@ static void openchannel_hook_cb(struct openchannel_hook_payload *payload, log_debug(openingd->ld->log, "openchannel_hook_cb says '%s'", errmsg); + our_upfront_shutdown_script = NULL; } else if (!json_tok_streq(buffer, t, "continue")) fatal("Plugin returned an invalid result for the " "openchannel hook: %.*s", t->end - t->start, buffer + t->start); - } + + /* Check for a 'close_to' address passed back */ + if (!errmsg) { + t = json_get_member(buffer, toks, "close_to"); + if (t) { + switch (json_to_address_scriptpubkey(tmpctx, chainparams, + buffer, t, + &our_upfront_shutdown_script)) { + case ADDRESS_PARSE_UNRECOGNIZED: + fatal("Plugin returned an invalid response to the" + " openchannel.close_to hook: %.*s", + t->end - t->start, buffer + t->start); + case ADDRESS_PARSE_WRONG_NETWORK: + fatal("Plugin returned invalid response to the" + " openchannel.close_to hook: address %s is" + " not on network %s", + tal_hex(NULL, our_upfront_shutdown_script), + chainparams->network_name); + case ADDRESS_PARSE_SUCCESS: + errmsg = NULL; + } + } else + our_upfront_shutdown_script = NULL; + } + } else + our_upfront_shutdown_script = NULL; subd_send_msg(openingd, - take(towire_opening_got_offer_reply(NULL, errmsg))); + take(towire_opening_got_offer_reply(NULL, errmsg, + our_upfront_shutdown_script))); } REGISTER_PLUGIN_HOOK(openchannel, @@ -808,7 +837,7 @@ static void opening_got_offer(struct subd *openingd, if (peer_active_channel(uc->peer)) { subd_send_msg(openingd, take(towire_opening_got_offer_reply(NULL, - "Already have active channel"))); + "Already have active channel", NULL))); return; } diff --git a/openingd/opening_wire.csv b/openingd/opening_wire.csv index ecc743443..23ca203d1 100644 --- a/openingd/opening_wire.csv +++ b/openingd/opening_wire.csv @@ -44,6 +44,8 @@ msgdata,opening_got_offer,shutdown_scriptpubkey,u8,shutdown_len # master->openingd: optional rejection message msgtype,opening_got_offer_reply,6105 msgdata,opening_got_offer_reply,rejection,?wirestring, +msgdata,opening_got_offer_reply,shutdown_len,u16, +msgdata,opening_got_offer_reply,our_shutdown_scriptpubkey,?u8,shutdown_len # Openingd->master: we've successfully offered channel. # This gives their sig, means we can broadcast tx: we're done. @@ -118,8 +120,10 @@ msgdata,opening_fundee,feerate_per_kw,u32, msgdata,opening_fundee,msglen,u16, msgdata,opening_fundee,funding_signed_msg,u8,msglen msgdata,opening_fundee,our_channel_reserve_satoshis,amount_sat, -msgdata,opening_fundee,shutdown_len,u16, -msgdata,opening_fundee,shutdown_scriptpubkey,u8,shutdown_len +msgdata,opening_fundee,local_shutdown_len,u16, +msgdata,opening_fundee,local_shutdown_scriptpubkey,u8,local_shutdown_len +msgdata,opening_fundee,remote_shutdown_len,u16, +msgdata,opening_fundee,remote_shutdown_scriptpubkey,u8,remote_shutdown_len # master -> openingd: do you have a memleak? msgtype,opening_dev_memleak,6033 diff --git a/openingd/openingd.c b/openingd/openingd.c index 6fd425b50..b61a36da8 100644 --- a/openingd/openingd.c +++ b/openingd/openingd.c @@ -878,7 +878,7 @@ static u8 *fundee_channel(struct state *state, const u8 *open_channel_msg) struct bitcoin_signature theirsig, sig; struct bitcoin_tx *local_commit, *remote_commit; struct bitcoin_blkid chain_hash; - u8 *msg; + u8 *msg, *our_upfront_shutdown_script; const u8 *wscript; u8 channel_flags; char* err_reason; @@ -1064,7 +1064,8 @@ static u8 *fundee_channel(struct state *state, const u8 *open_channel_msg) wire_sync_write(REQ_FD, take(msg)); msg = wire_sync_read(tmpctx, REQ_FD); - if (!fromwire_opening_got_offer_reply(tmpctx, msg, &err_reason)) + if (!fromwire_opening_got_offer_reply(NULL, msg, &err_reason, + &our_upfront_shutdown_script)) master_badmsg(WIRE_OPENING_GOT_OFFER_REPLY, msg); /* If they give us a reason to reject, do so. */ @@ -1075,6 +1076,9 @@ static u8 *fundee_channel(struct state *state, const u8 *open_channel_msg) return NULL; } + if (!our_upfront_shutdown_script) + our_upfront_shutdown_script = dev_upfront_shutdown_script(state); + /* OK, we accept! */ msg = towire_accept_channel_option_upfront_shutdown_script(NULL, &state->channel_id, state->localconf.dust_limit, @@ -1090,7 +1094,7 @@ static u8 *fundee_channel(struct state *state, const u8 *open_channel_msg) &state->our_points.delayed_payment, &state->our_points.htlc, &state->first_per_commitment_point[LOCAL], - dev_upfront_shutdown_script(tmpctx)); + our_upfront_shutdown_script); sync_crypto_write(state->pps, take(msg)); @@ -1258,6 +1262,7 @@ static u8 *fundee_channel(struct state *state, const u8 *open_channel_msg) state->feerate_per_kw, msg, state->localconf.channel_reserve, + our_upfront_shutdown_script, state->remote_upfront_shutdown_script); } diff --git a/tests/plugins/accepter_close_to.py b/tests/plugins/accepter_close_to.py new file mode 100755 index 000000000..1b027b74f --- /dev/null +++ b/tests/plugins/accepter_close_to.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Simple plugin to test the openchannel_hook's + 'close_to' address functionality. + + If the funding amount is: + - a multiple of 11: we send back a valid address (regtest) + - a multiple of 7: we send back an empty address + - a multiple of 5: we send back an address for the wrong chain (mainnet) + - otherwise: we don't include the close_to +""" + +from lightning import Plugin, Millisatoshi + +plugin = Plugin() + + +@plugin.hook('openchannel') +def on_openchannel(openchannel, plugin, **kwargs): + # - a multiple of 11: we send back a valid address (regtest) + if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() % 11 == 0: + return {'result': 'continue', 'close_to': 'bcrt1q7gtnxmlaly9vklvmfj06amfdef3rtnrdazdsvw'} + + # - a multiple of 7: we send back an empty address + if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() % 7 == 0: + return {'result': 'continue', 'close_to': ''} + + # - a multiple of 5: we send back an address for the wrong chain (mainnet) + if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() % 5 == 0: + return {'result': 'continue', 'close_to': 'bc1qlq8srqnz64wgklmqvurv7qnr4rvtq2u96hhfg2'} + + # - otherwise: we don't include the close_to + return {'result': 'continue'} + + +plugin.run() diff --git a/tests/test_connection.py b/tests/test_connection.py index 8746df34b..9474ac32c 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1079,13 +1079,21 @@ def test_funding_cancel_race(node_factory, bitcoind, executor): @unittest.skipIf(TEST_NETWORK != 'regtest', "External wallet support doesn't work with elements yet.") def test_funding_close_upfront(node_factory, bitcoind): l1 = node_factory.get_node() - l2 = node_factory.get_node() - def _fundchannel(l1, l2, close_to): + opts = {'plugin': os.path.join(os.getcwd(), 'tests/plugins/accepter_close_to.py')} + l2 = node_factory.get_node(options=opts) + + # The 'accepter_close_to' plugin uses the channel funding amount to determine + # whether or not to include a 'close_to' address + amt_normal = 100007 # continues without returning a close_to + amt_addr = 100001 # returns valid regtest address + + remote_valid_addr = 'bcrt1q7gtnxmlaly9vklvmfj06amfdef3rtnrdazdsvw' + + def _fundchannel(l1, l2, amount, close_to): l1.rpc.connect(l2.info['id'], 'localhost', l2.port) assert(l1.rpc.listpeers()['peers'][0]['id'] == l2.info['id']) - amount = 2**24 - 1 resp = l1.rpc.fundchannel_start(l2.info['id'], amount, close_to=close_to) address = resp['funding_address'] @@ -1123,16 +1131,16 @@ def test_funding_close_upfront(node_factory, bitcoind): for node in [l1, l2]: node.daemon.wait_for_log(r'State changed from CHANNELD_AWAITING_LOCKIN to CHANNELD_NORMAL') - channel = node.rpc.listpeers()['peers'][0]['channels'][0] + channel = node.rpc.listpeers()['peers'][0]['channels'][-1] assert amount * 1000 == channel['msatoshi_total'] # check that normal peer close works - _fundchannel(l1, l2, None) + _fundchannel(l1, l2, amt_normal, None) assert l1.rpc.close(l2.info['id'])['type'] == 'mutual' # check that you can provide a closing address upfront addr = l1.rpc.newaddr()['bech32'] - _fundchannel(l1, l2, addr) + _fundchannel(l1, l2, amt_normal, addr) # confirm that it appears in listpeers assert addr == only_one(l1.rpc.listpeers()['peers'])['channels'][1]['close_to_addr'] resp = l1.rpc.close(l2.info['id']) @@ -1141,21 +1149,28 @@ def test_funding_close_upfront(node_factory, bitcoind): # check that passing in the same addr to close works addr = bitcoind.rpc.getnewaddress() - _fundchannel(l1, l2, addr) + _fundchannel(l1, l2, amt_normal, addr) assert addr == only_one(l1.rpc.listpeers()['peers'])['channels'][2]['close_to_addr'] resp = l1.rpc.close(l2.info['id'], destination=addr) assert resp['type'] == 'mutual' assert only_one(only_one(bitcoind.rpc.decoderawtransaction(resp['tx'])['vout'])['scriptPubKey']['addresses']) == addr - # check that remote peer closing works as expected - _fundchannel(l1, l2, addr) + # check that remote peer closing works as expected (and that remote's close_to works) + _fundchannel(l1, l2, amt_addr, addr) + # send some money to remote so that they have a closeout + l1.rpc.pay(l2.rpc.invoice((amt_addr // 2) * 1000, 'test_remote_close_to', 'desc')['bolt11']) + assert only_one(l2.rpc.listpeers()['peers'])['channels'][-1]['close_to_addr'] == remote_valid_addr + resp = l2.rpc.close(l1.info['id']) assert resp['type'] == 'mutual' - assert only_one(only_one(bitcoind.rpc.decoderawtransaction(resp['tx'])['vout'])['scriptPubKey']['addresses']) == addr + vouts = bitcoind.rpc.decoderawtransaction(resp['tx'])['vout'] + assert len(vouts) == 2 + for vout in vouts: + assert only_one(vout['scriptPubKey']['addresses']) in [addr, remote_valid_addr] # check that passing in a different addr to close causes an RPC error addr2 = l1.rpc.newaddr()['bech32'] - _fundchannel(l1, l2, addr) + _fundchannel(l1, l2, amt_normal, addr) with pytest.raises(RpcError, match=r'does not match previous shutdown script'): l1.rpc.close(l2.info['id'], destination=addr2)