Browse Source

openingd: add openchannel hook.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
htlc_accepted_hook
Rusty Russell 6 years ago
committed by neil saitug
parent
commit
e5b5f1d7e5
  1. 2
      CHANGELOG.md
  2. 34
      doc/PLUGINS.md
  3. 137
      lightningd/opening_control.c
  4. 24
      tests/plugins/reject_odd_funding_amounts.py
  5. 41
      tests/test_plugin.py

2
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))

34
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

137
lightningd/opening_control.c

@ -25,6 +25,7 @@
#include <lightningd/opening_control.h>
#include <lightningd/peer_comms.h>
#include <lightningd/peer_control.h>
#include <lightningd/plugin_hook.h>
#include <lightningd/subd.h>
#include <openingd/gen_opening_wire.h>
#include <wire/wire.h>
@ -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,

24
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()

41
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)

Loading…
Cancel
Save