From e5b5f1d7e5dd0f6fc62ede71694a77125af85618 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 21 May 2019 08:15:20 +0930 Subject: [PATCH] openingd: add openchannel hook. Signed-off-by: Rusty Russell --- CHANGELOG.md | 2 +- doc/PLUGINS.md | 34 +++++ lightningd/opening_control.c | 137 +++++++++++++++++++- tests/plugins/reject_odd_funding_amounts.py | 24 ++++ tests/test_plugin.py | 41 ++++++ 5 files changed, 232 insertions(+), 6 deletions(-) create mode 100755 tests/plugins/reject_odd_funding_amounts.py diff --git a/CHANGELOG.md b/CHANGELOG.md index de5d0dc81..5dd5df645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - JSON API: `listpeers` status now shows how many confirmations until channel is open (#2405) - Config: Adds parameter `min-capacity-sat` to reject tiny channels. - JSON API: `listforwards` now includes the time an HTLC was received and when it was resolved. Both are expressed as UNIX timestamps to facilitate parsing (Issue [#2491](https://github.com/ElementsProject/lightning/issues/2491), PR [#2528](https://github.com/ElementsProject/lightning/pull/2528)) -- JSON API: new plugin `invoice_payment` hook for intercepting invoices before they're paid. ++- JSON API: new plugin hooks `invoice_payment` for intercepting invoices before they're paid, and `openchannel` for intercepting channel opens. - plugin: the `connected` hook can now send an `error_message` to the rejected peer. - Protocol: we now enforce `option_upfront_shutdown_script` if a peer negotiates it. - JSON API: `listforwards` now includes the local_failed forwards with failcode (Issue [#2435](https://github.com/ElementsProject/lightning/issues/2435), PR [#2524](https://github.com/ElementsProject/lightning/pull/2524)) diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index 2a4987d14..21792f3e2 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -314,6 +314,40 @@ It can return a non-zero `failure_code` field as defined for final nodes in [BOLT 4][bolt4-failure-codes], or otherwise an empty object to accept the payment. + +#### `openchannel` + +This hook is called whenever a remote peer tries to fund a channel to us, +and it has passed basic sanity checks: + +```json +{ + "openchannel": { + "id": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "funding_satoshis": "100000000msat", + "push_msat": "0msat", + "dust_limit_satoshis": "546000msat", + "max_htlc_value_in_flight_msat": "18446744073709551615msat", + "channel_reserve_satoshis": "1000000msat", + "htlc_minimum_msat": "0msat", + "feerate_per_kw": 7500, + "to_self_delay": 5, + "max_accepted_htlcs": 483, + "channel_flags": 1 + } +} +``` + +There may be additional fields, including `shutdown_scriptpubkey` and +a hex-string. You can see the definitions of these fields in [BOLT 2's description of the open_channel message][bolt2-open-channel]. + +The returned result must contain a `result` member which is either +the string `reject` or `continue`. If `reject` and +there's a member `error_message`, that member is sent to the peer +before disconnection. + + [jsonrpc-spec]: https://www.jsonrpc.org/specification [jsonrpc-notification-spec]: https://www.jsonrpc.org/specification#notification [bolt4-failure-codes]: https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#failure-messages +[bolt2-open-channel]: https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#the-open_channel-message diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c index be2da28fe..76f198ccd 100644 --- a/lightningd/opening_control.c +++ b/lightningd/opening_control.c @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -725,18 +726,144 @@ static void channel_config(struct lightningd *ld, ours->channel_reserve = AMOUNT_SAT(UINT64_MAX); } +struct openchannel_hook_payload { + struct subd *openingd; + struct amount_sat funding_satoshis; + struct amount_msat push_msat; + struct amount_sat dust_limit_satoshis; + struct amount_msat max_htlc_value_in_flight_msat; + struct amount_sat channel_reserve_satoshis; + struct amount_msat htlc_minimum_msat; + u32 feerate_per_kw; + u16 to_self_delay; + u16 max_accepted_htlcs; + u8 channel_flags; + u8 *shutdown_scriptpubkey; +}; + +static void +openchannel_hook_serialize(struct openchannel_hook_payload *payload, + struct json_stream *stream) +{ + struct uncommitted_channel *uc = payload->openingd->channel; + json_object_start(stream, "openchannel"); + json_add_node_id(stream, "id", &uc->peer->id); + json_add_amount_sat_only(stream, "funding_satoshis", + payload->funding_satoshis); + json_add_amount_msat_only(stream, "push_msat", payload->push_msat); + json_add_amount_sat_only(stream, "dust_limit_satoshis", + payload->dust_limit_satoshis); + json_add_amount_msat_only(stream, "max_htlc_value_in_flight_msat", + payload->max_htlc_value_in_flight_msat); + json_add_amount_sat_only(stream, "channel_reserve_satoshis", + payload->channel_reserve_satoshis); + json_add_amount_msat_only(stream, "htlc_minimum_msat", + payload->htlc_minimum_msat); + json_add_num(stream, "feerate_per_kw", payload->feerate_per_kw); + json_add_num(stream, "to_self_delay", payload->to_self_delay); + json_add_num(stream, "max_accepted_htlcs", payload->max_accepted_htlcs); + json_add_num(stream, "channel_flags", payload->channel_flags); + if (tal_count(payload->shutdown_scriptpubkey) != 0) + json_add_hex(stream, "shutdown_scriptpubkey", + payload->shutdown_scriptpubkey, + tal_count(payload->shutdown_scriptpubkey)); + json_object_end(stream); /* .openchannel */ +} + +/* openingd dies? Remove openingd ptr from payload */ +static void openchannel_payload_remove_openingd(struct subd *openingd, + struct openchannel_hook_payload *payload) +{ + assert(payload->openingd == openingd); + payload->openingd = NULL; +} + +static void openchannel_hook_cb(struct openchannel_hook_payload *payload, + const char *buffer, + const jsmntok_t *toks) +{ + struct subd *openingd = payload->openingd; + const char *errmsg = NULL; + + /* We want to free this, whatever happens. */ + tal_steal(tmpctx, payload); + + /* If openingd went away, don't send it anything! */ + if (!openingd) + return; + + tal_del_destructor2(openingd, openchannel_payload_remove_openingd, payload); + + /* If we had a hook, check what it says */ + if (buffer) { + const jsmntok_t *t = json_get_member(buffer, toks, "result"); + if (!t) + fatal("Plugin returned an invalid response to the" + " openchannel hook: %.*s", + toks[0].end - toks[0].start, + buffer + toks[0].start); + + if (json_tok_streq(buffer, t, "reject")) { + t = json_get_member(buffer, toks, "error_message"); + if (t) + errmsg = json_strdup(tmpctx, buffer, t); + else + errmsg = ""; + log_debug(openingd->ld->log, + "openchannel_hook_cb says '%s'", + errmsg); + } 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); + } + + subd_send_msg(openingd, + take(towire_opening_got_offer_reply(NULL, errmsg))); +} + +REGISTER_PLUGIN_HOOK(openchannel, + openchannel_hook_cb, + struct openchannel_hook_payload *, + openchannel_hook_serialize, + struct openchannel_hook_payload *); + static void opening_got_offer(struct subd *openingd, const u8 *msg, struct uncommitted_channel *uc) { - const char *reason = NULL; + struct openchannel_hook_payload *payload; /* Tell them they can't open, if we already have open channel. */ - if (peer_active_channel(uc->peer)) - reason = "Already have active channel"; + if (peer_active_channel(uc->peer)) { + subd_send_msg(openingd, + take(towire_opening_got_offer_reply(NULL, + "Already have active channel"))); + return; + } - subd_send_msg(openingd, - take(towire_opening_got_offer_reply(NULL, reason))); + payload = tal(openingd->ld, struct openchannel_hook_payload); + payload->openingd = openingd; + if (!fromwire_opening_got_offer(payload, msg, + &payload->funding_satoshis, + &payload->push_msat, + &payload->dust_limit_satoshis, + &payload->max_htlc_value_in_flight_msat, + &payload->channel_reserve_satoshis, + &payload->htlc_minimum_msat, + &payload->feerate_per_kw, + &payload->to_self_delay, + &payload->max_accepted_htlcs, + &payload->channel_flags, + &payload->shutdown_scriptpubkey)) { + log_broken(openingd->log, "Malformed opening_got_offer %s", + tal_hex(tmpctx, msg)); + tal_free(openingd); + return; + } + + tal_add_destructor2(openingd, openchannel_payload_remove_openingd, payload); + plugin_hook_call_openchannel(openingd->ld, payload, payload); } static unsigned int openingd_msg(struct subd *openingd, diff --git a/tests/plugins/reject_odd_funding_amounts.py b/tests/plugins/reject_odd_funding_amounts.py new file mode 100755 index 000000000..76c295da2 --- /dev/null +++ b/tests/plugins/reject_odd_funding_amounts.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""Simple plugin to test the openchannel_hook. + +We just refuse to let them open channels with an odd amount of millisatoshis. +""" + +from lightning import Plugin, Millisatoshi + +plugin = Plugin() + + +@plugin.hook('openchannel') +def on_openchannel(openchannel, plugin): + print("{} VARS".format(len(openchannel.keys()))) + for k in sorted(openchannel.keys()): + print("{}={}".format(k, openchannel[k])) + + if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() % 2 == 1: + return {'result': 'reject', 'error_message': "I don't like odd amounts"} + + return {'result': 'continue'} + + +plugin.run() diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 73aa82653..ae9d1a3c2 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -268,3 +268,44 @@ def test_invoice_payment_hook(node_factory): l2.daemon.wait_for_log('label=label2') l2.daemon.wait_for_log('msat=') l2.daemon.wait_for_log('preimage=' + '0' * 64) + + +def test_openchannel_hook(node_factory, bitcoind): + """ l2 uses the reject_odd_funding_amounts plugin to reject some openings. + """ + opts = [{}, {'plugin': 'tests/plugins/reject_odd_funding_amounts.py'}] + l1, l2 = node_factory.line_graph(2, fundchannel=False, opts=opts) + + # Get some funds. + addr = l1.rpc.newaddr()['bech32'] + bitcoind.rpc.sendtoaddress(addr, 10) + numfunds = len(l1.rpc.listfunds()['outputs']) + bitcoind.generate_block(1) + wait_for(lambda: len(l1.rpc.listfunds()['outputs']) > numfunds) + + # Even amount: works. + l1.rpc.fundchannel(l2.info['id'], 100000) + + # Make sure plugin got all the vars we expect + l2.daemon.wait_for_log('reject_odd_funding_amounts.py 11 VARS') + l2.daemon.wait_for_log('reject_odd_funding_amounts.py channel_flags=1') + l2.daemon.wait_for_log('reject_odd_funding_amounts.py channel_reserve_satoshis=1000000msat') + l2.daemon.wait_for_log('reject_odd_funding_amounts.py dust_limit_satoshis=546000msat') + l2.daemon.wait_for_log('reject_odd_funding_amounts.py feerate_per_kw=7500') + l2.daemon.wait_for_log('reject_odd_funding_amounts.py funding_satoshis=100000000msat') + l2.daemon.wait_for_log('reject_odd_funding_amounts.py htlc_minimum_msat=0msat') + l2.daemon.wait_for_log('reject_odd_funding_amounts.py id={}'.format(l1.info['id'])) + l2.daemon.wait_for_log('reject_odd_funding_amounts.py max_accepted_htlcs=483') + l2.daemon.wait_for_log('reject_odd_funding_amounts.py max_htlc_value_in_flight_msat=18446744073709551615msat') + l2.daemon.wait_for_log('reject_odd_funding_amounts.py push_msat=0msat') + l2.daemon.wait_for_log('reject_odd_funding_amounts.py to_self_delay=5') + + # Close it. + l1.rpc.close(l2.info['id']) + bitcoind.generate_block(1) + wait_for(lambda: [c['state'] for c in only_one(l1.rpc.listpeers(l2.info['id'])['peers'])['channels']] == ['ONCHAIN']) + + # Odd amount: fails + l1.connect(l2) + with pytest.raises(RpcError, match=r"I don't like odd amounts"): + l1.rpc.fundchannel(l2.info['id'], 100001)