mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-03-03 10:46:58 +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)
|
||||
- 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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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, 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,
|
||||
|
|
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('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…
Add table
Reference in a new issue