mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-03-03 18:57:06 +01:00
openingd: add openchannel hook.
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
parent
401bd9f8ef
commit
e5b5f1d7e5
5 changed files with 232 additions and 6 deletions
|
@ -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)
|
- JSON API: `listpeers` status now shows how many confirmations until channel is open (#2405)
|
||||||
- Config: Adds parameter `min-capacity-sat` to reject tiny channels.
|
- 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: `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.
|
- 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.
|
- 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))
|
- 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))
|
||||||
|
|
|
@ -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
|
nodes in [BOLT 4][bolt4-failure-codes], or otherwise an empty object
|
||||||
to accept the payment.
|
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-spec]: https://www.jsonrpc.org/specification
|
||||||
[jsonrpc-notification-spec]: https://www.jsonrpc.org/specification#notification
|
[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
|
[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
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
#include <lightningd/opening_control.h>
|
#include <lightningd/opening_control.h>
|
||||||
#include <lightningd/peer_comms.h>
|
#include <lightningd/peer_comms.h>
|
||||||
#include <lightningd/peer_control.h>
|
#include <lightningd/peer_control.h>
|
||||||
|
#include <lightningd/plugin_hook.h>
|
||||||
#include <lightningd/subd.h>
|
#include <lightningd/subd.h>
|
||||||
#include <openingd/gen_opening_wire.h>
|
#include <openingd/gen_opening_wire.h>
|
||||||
#include <wire/wire.h>
|
#include <wire/wire.h>
|
||||||
|
@ -725,18 +726,144 @@ static void channel_config(struct lightningd *ld,
|
||||||
ours->channel_reserve = AMOUNT_SAT(UINT64_MAX);
|
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,
|
static void opening_got_offer(struct subd *openingd,
|
||||||
const u8 *msg,
|
const u8 *msg,
|
||||||
struct uncommitted_channel *uc)
|
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. */
|
/* Tell them they can't open, if we already have open channel. */
|
||||||
if (peer_active_channel(uc->peer))
|
if (peer_active_channel(uc->peer)) {
|
||||||
reason = "Already have active channel";
|
|
||||||
|
|
||||||
subd_send_msg(openingd,
|
subd_send_msg(openingd,
|
||||||
take(towire_opening_got_offer_reply(NULL, reason)));
|
take(towire_opening_got_offer_reply(NULL,
|
||||||
|
"Already have active channel")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
static unsigned int openingd_msg(struct subd *openingd,
|
||||||
|
|
24
tests/plugins/reject_odd_funding_amounts.py
Executable file
24
tests/plugins/reject_odd_funding_amounts.py
Executable file
|
@ -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()
|
|
@ -268,3 +268,44 @@ def test_invoice_payment_hook(node_factory):
|
||||||
l2.daemon.wait_for_log('label=label2')
|
l2.daemon.wait_for_log('label=label2')
|
||||||
l2.daemon.wait_for_log('msat=')
|
l2.daemon.wait_for_log('msat=')
|
||||||
l2.daemon.wait_for_log('preimage=' + '0' * 64)
|
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…
Add table
Reference in a new issue