From b581028cc61a9925923ae1e7fb8fb1ccbcd49a9d Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Thu, 28 Nov 2019 19:07:52 +0100 Subject: [PATCH 01/15] wire: Add common messages that are independent of daemons These messages may be exchanged between the master and any daemon. For now these are just the daemons that a peer may be attached to at any time since the first example of this is the custommsg infrastructure. --- channeld/channeld.c | 16 ++++++++++++++++ lightningd/channel_control.c | 14 ++++++++++++++ lightningd/opening_control.c | 15 +++++++++++++++ openingd/openingd.c | 15 +++++++++++++++ wire/Makefile | 11 +++++++++-- wire/common_wire_csv | 10 ++++++++++ 6 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 wire/common_wire_csv diff --git a/channeld/channeld.c b/channeld/channeld.c index 30ae01f84190..6506ae0dbea4 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -55,6 +55,7 @@ #include #include #include +#include #include #include #include @@ -2911,6 +2912,21 @@ static void req_in(struct peer *peer, const u8 *msg) case WIRE_CHANNEL_SEND_ERROR_REPLY: break; } + + /* Now handle common messages. */ + switch ((enum common_wire_type)t) { +#if DEVELOPER + case WIRE_CUSTOMMSG_OUT: + /* TODO(cdecker) Add handling of custom messages. */ + return; +#else + case WIRE_CUSTOMMSG_OUT: +#endif + /* We send these. */ + case WIRE_CUSTOMMSG_IN: + break; + } + master_badmsg(-1, msg); } diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index f59d56795b50..9256151bf99e 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -23,6 +23,7 @@ #include #include #include +#include #include static void update_feerates(struct lightningd *ld, struct channel *channel) @@ -319,6 +320,19 @@ static unsigned channel_msg(struct subd *sd, const u8 *msg, const int *fds) break; } + switch ((enum common_wire_type)t) { +#if DEVELOPER + case WIRE_CUSTOMMSG_IN: + /* TODO(cdecker) Add handling of custom messages. */ + break; +#else + case WIRE_CUSTOMMSG_IN: +#endif + /* We send these. */ + case WIRE_CUSTOMMSG_OUT: + break; + } + return 0; } diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c index 614293484ed8..27919a75af77 100644 --- a/lightningd/opening_control.c +++ b/lightningd/opening_control.c @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -922,6 +923,20 @@ static unsigned int openingd_msg(struct subd *openingd, case WIRE_OPENING_DEV_MEMLEAK_REPLY: break; } + + switch ((enum common_wire_type)t) { +#if DEVELOPER + case WIRE_CUSTOMMSG_IN: + /* TODO(cdecker) Add handling of custom messages. */ + return 0; +#else + case WIRE_CUSTOMMSG_IN: +#endif + /* We send these. */ + case WIRE_CUSTOMMSG_OUT: + break; + } + log_broken(openingd->log, "Unexpected msg %s: %s", opening_wire_type_name(t), tal_hex(tmpctx, msg)); tal_free(openingd); diff --git a/openingd/openingd.c b/openingd/openingd.c index 3b9e5258e4db..380d56a22878 100644 --- a/openingd/openingd.c +++ b/openingd/openingd.c @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -1413,6 +1414,20 @@ static u8 *handle_master_in(struct state *state) break; } + /* Now handle common messages. */ + switch ((enum common_wire_type)t) { +#if DEVELOPER + case WIRE_CUSTOMMSG_OUT: + /* TODO(cdecker) Add handling of custom messages. */ + return NULL; +#else + case WIRE_CUSTOMMSG_OUT: +#endif + /* We send these. */ + case WIRE_CUSTOMMSG_IN: + break; + } + status_failed(STATUS_FAIL_MASTER_IO, "Unknown msg %s", tal_hex(tmpctx, msg)); } diff --git a/wire/Makefile b/wire/Makefile index 8a0efb011728..92bec190bdc8 100644 --- a/wire/Makefile +++ b/wire/Makefile @@ -10,8 +10,8 @@ WIRE_HEADERS_NOGEN := wire/onion_defs.h \ wire/wire.h \ wire/wire_sync.h \ wire/wire_io.h -WIRE_GEN_HEADERS := wire/gen_peer_wire.h wire/gen_onion_wire.h -WIRE_GEN_SRC := wire/gen_peer_wire.c +WIRE_GEN_HEADERS := wire/gen_peer_wire.h wire/gen_onion_wire.h wire/gen_common_wire.h +WIRE_GEN_SRC := wire/gen_peer_wire.c wire/gen_common_wire.c WIRE_GEN_ONION_SRC := wire/gen_onion_wire.c WIRE_SRC := wire/wire_sync.c \ wire/wire_io.c \ @@ -85,6 +85,13 @@ wire/gen_onion_wire.h: wire/gen_onion_wire_csv $(WIRE_BOLT_DEPS) wire/Makefile wire/gen_onion_wire.c: wire/gen_onion_wire_csv $(WIRE_BOLT_DEPS) wire/Makefile $(BOLT_GEN) -s --expose-tlv-type=tlv_payload --page impl ${@:.c=.h} onion_type < $< > $@ +# Some messages that are common among all daemons +wire/gen_common_wire.h: wire/common_wire_csv $(WIRE_BOLT_DEPS) wire/Makefile + $(BOLT_GEN) -s --page header $@ common_wire_type < $< > $@ + +wire/gen_common_wire.c: wire/common_wire_csv $(WIRE_BOLT_DEPS) wire/Makefile + $(BOLT_GEN) -s --page impl ${@:.c=.h} common_wire_type < $< > $@ + check-source: $(WIRE_SRC:%=check-src-include-order/%) $(WIRE_HEADERS_NOGEN:%=check-hdr-include-order/%) check-source-bolt: $(WIRE_SRC:%=bolt-check/%) $(WIRE_HEADERS_NOGEN:%=bolt-check/%) diff --git a/wire/common_wire_csv b/wire/common_wire_csv new file mode 100644 index 000000000000..7e607c806ccc --- /dev/null +++ b/wire/common_wire_csv @@ -0,0 +1,10 @@ +# A custom message that we got from a peer and don't know how to handle, so we +# forward it to the master for further handling. +msgtype,custommsg_in,1030 +msgdata,custommsg_in,msg_len,u16, +msgdata,custommsg_in,msg,u8,msg_len + +# A custom message that the master tells us to send to the peer. +msgtype,custommsg_out,1031 +msgdata,custommsg_out,msg_len,u16, +msgdata,custommsg_out,msg,u8,msg_len From 0ba41a4a35b17875b6cf9c6dc87deb3618955ded Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Tue, 3 Dec 2019 15:48:18 +0100 Subject: [PATCH 02/15] subd: Add a helper to get the current owning subd for a given peer This is currently in opening_control since that's the only part that has access to the uncommitted_channel internals. Otherwise it's independent from the specific daemon. --- lightningd/opening_control.c | 14 +++++++++++++- lightningd/opening_control.h | 3 +++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c index 27919a75af77..66eaed5f84cd 100644 --- a/lightningd/opening_control.c +++ b/lightningd/opening_control.c @@ -29,7 +29,6 @@ #include #include #include -#include #include #include #include @@ -1338,3 +1337,16 @@ void opening_dev_memleak(struct command *cmd) opening_memleak_req_next(cmd, NULL); } #endif /* DEVELOPER */ + +struct subd *peer_get_owning_subd(struct peer *peer) +{ + struct channel *channel; + channel = peer_active_channel(peer); + + if (channel != NULL) { + return channel->owner; + } else if (peer->uncommitted_channel != NULL) { + return peer->uncommitted_channel->openingd; + } + return NULL; +} diff --git a/lightningd/opening_control.h b/lightningd/opening_control.h index 23df19eb3aee..fbf8ce7156a2 100644 --- a/lightningd/opening_control.h +++ b/lightningd/opening_control.h @@ -2,6 +2,7 @@ #define LIGHTNING_LIGHTNINGD_OPENING_CONTROL_H #include "config.h" #include +#include struct channel_id; struct crypto_state; @@ -26,4 +27,6 @@ struct command; void opening_dev_memleak(struct command *cmd); #endif +struct subd *peer_get_owning_subd(struct peer *peer); + #endif /* LIGHTNING_LIGHTNINGD_OPENING_CONTROL_H */ From 2e5328d04832756e8765eae1f76c40bb0c26b605 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 4 Dec 2019 16:21:31 +0100 Subject: [PATCH 03/15] subd: Allow sending common messages to subdaemons --- lightningd/subd.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lightningd/subd.c b/lightningd/subd.c index 1c1779cbfbf9..3db01522d6bf 100644 --- a/lightningd/subd.c +++ b/lightningd/subd.c @@ -27,6 +27,7 @@ #include #include #include +#include #include static bool move_fd(int from, int to) @@ -741,9 +742,11 @@ struct subd *new_channel_subd_(struct lightningd *ld, void subd_send_msg(struct subd *sd, const u8 *msg_out) { + u16 type = fromwire_peektype(msg_out); /* FIXME: We should use unique upper bits for each daemon, then * have generate-wire.py add them, just assert here. */ - assert(!strstarts(sd->msgname(fromwire_peektype(msg_out)), "INVALID")); + assert(!strstarts(common_wire_type_name(type), "INVALID") || + !strstarts(sd->msgname(type), "INVALID")); msg_enqueue(sd->outq, msg_out); } From 32a241d9d9a69073a573e879681635bc04b8bb70 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 4 Dec 2019 16:26:57 +0100 Subject: [PATCH 04/15] json-rpc: Add sendcustommsg command This command injects a custom message into the encrypted transport stream to the peer, allowing users to build custom protocols on top of c-lightning without requiring any changes to c-lightning itself. --- lightningd/peer_control.c | 67 +++++++++++++++++++++ lightningd/test/run-invoice-select-inchan.c | 8 +++ wallet/test/run-wallet.c | 11 ++++ 3 files changed, 86 insertions(+) diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index 4116dae7b16e..d56ca62077b3 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -54,6 +54,7 @@ #include #include #include +#include #include #include @@ -2373,5 +2374,71 @@ void peer_dev_memleak(struct command *cmd) { peer_memleak_req_next(cmd, NULL); } + +static struct command_result *json_sendcustommsg(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + struct json_stream *response; + struct node_id *dest; + struct peer *peer; + struct subd *owner; + u8 *msg; + + if (!param(cmd, buffer, params, + p_req("node_id", param_node_id, &dest), + p_req("msg", param_bin_from_hex, &msg), + NULL)) + return command_param_failed(); + + peer = peer_by_id(cmd->ld, dest); + if (!peer) { + return command_fail(cmd, JSONRPC2_INVALID_REQUEST, + "No such peer: %s", + type_to_string(cmd, struct node_id, dest)); + } + + owner = peer_get_owning_subd(peer); + if (owner == NULL) { + return command_fail(cmd, JSONRPC2_INVALID_REQUEST, + "Peer is not connected: %s", + type_to_string(cmd, struct node_id, dest)); + } + + /* Only a couple of subdaemons have the ability to send custom + * messages. We whitelist those, and error if the current owner is not + * in the whitelist. The reason is that some subdaemons do not handle + * spontaneous messages from the master well (I'm looking at you + * `closingd`...). */ + if (!streq(owner->name, "channeld") && + !streq(owner->name, "openingd")) { + return command_fail(cmd, JSONRPC2_INVALID_REQUEST, + "Peer is currently owned by %s which does " + "not support injecting custom messages.", + owner->name); + } + + subd_send_msg(owner, take(towire_custommsg_out(cmd, msg))); + + response = json_stream_success(cmd); + json_add_string(response, "status", + tal_fmt(cmd, + "Message sent to subdaemon %s for delivery", + owner->name)); + + return command_success(cmd, response); +} + +static const struct json_command sendcustommsg_command = { + "dev-sendcustommsg", + "utility", + json_sendcustommsg, + "Send a custom message to the peer with the given {node_id}", + .verbose = "dev-sendcustommsg node_id hexcustommsg", +}; + +AUTODATA(json_command, &sendcustommsg_command); + #endif /* DEVELOPER */ diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index f49a19f30c19..367b79b4fe5b 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -330,6 +330,11 @@ struct command_result *param_array(struct command *cmd UNNEEDED, const char *nam const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, const jsmntok_t **arr UNNEEDED) { fprintf(stderr, "param_array called!\n"); abort(); } +/* Generated stub for param_bin_from_hex */ +struct command_result *param_bin_from_hex(struct command *cmd UNNEEDED, const char *name UNNEEDED, + const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + u8 **bin UNNEEDED) +{ fprintf(stderr, "param_bin_from_hex called!\n"); abort(); } /* Generated stub for param_bitcoin_address */ struct command_result *param_bitcoin_address(struct command *cmd UNNEEDED, const char *name UNNEEDED, @@ -407,6 +412,9 @@ struct command_result *param_u64(struct command *cmd UNNEEDED, const char *name const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, uint64_t **num UNNEEDED) { fprintf(stderr, "param_u64 called!\n"); abort(); } +/* Generated stub for peer_get_owning_subd */ +struct subd *peer_get_owning_subd(struct peer *peer UNNEEDED) +{ fprintf(stderr, "peer_get_owning_subd called!\n"); abort(); } /* Generated stub for peer_memleak_done */ void peer_memleak_done(struct command *cmd UNNEEDED, struct subd *leaker UNNEEDED) { fprintf(stderr, "peer_memleak_done called!\n"); abort(); } diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 3e1d48356889..4709f4b1cbba 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -420,6 +420,11 @@ void outpointfilter_remove(struct outpointfilter *of UNNEEDED, bool param(struct command *cmd UNNEEDED, const char *buffer UNNEEDED, const jsmntok_t params[] UNNEEDED, ...) { fprintf(stderr, "param called!\n"); abort(); } +/* Generated stub for param_bin_from_hex */ +struct command_result *param_bin_from_hex(struct command *cmd UNNEEDED, const char *name UNNEEDED, + const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + u8 **bin UNNEEDED) +{ fprintf(stderr, "param_bin_from_hex called!\n"); abort(); } /* Generated stub for param_bitcoin_address */ struct command_result *param_bitcoin_address(struct command *cmd UNNEEDED, const char *name UNNEEDED, @@ -491,6 +496,9 @@ void payment_store(struct lightningd *ld UNNEEDED, struct wallet_payment *paymen void payment_succeeded(struct lightningd *ld UNNEEDED, struct htlc_out *hout UNNEEDED, const struct preimage *rval UNNEEDED) { fprintf(stderr, "payment_succeeded called!\n"); abort(); } +/* Generated stub for peer_get_owning_subd */ +struct subd *peer_get_owning_subd(struct peer *peer UNNEEDED) +{ fprintf(stderr, "peer_get_owning_subd called!\n"); abort(); } /* Generated stub for peer_memleak_done */ void peer_memleak_done(struct command *cmd UNNEEDED, struct subd *leaker UNNEEDED) { fprintf(stderr, "peer_memleak_done called!\n"); abort(); } @@ -590,6 +598,9 @@ u8 *towire_connectctl_connect_to_peer(const tal_t *ctx UNNEEDED, const struct no /* Generated stub for towire_connectctl_peer_disconnected */ u8 *towire_connectctl_peer_disconnected(const tal_t *ctx UNNEEDED, const struct node_id *id UNNEEDED) { fprintf(stderr, "towire_connectctl_peer_disconnected called!\n"); abort(); } +/* Generated stub for towire_custommsg_out */ +u8 *towire_custommsg_out(const tal_t *ctx UNNEEDED, const u8 *msg UNNEEDED) +{ fprintf(stderr, "towire_custommsg_out called!\n"); abort(); } /* Generated stub for towire_errorfmt */ u8 *towire_errorfmt(const tal_t *ctx UNNEEDED, const struct channel_id *channel UNNEEDED, From d74bb3244d8c701ba27d35301f6e12e8113b385e Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 4 Dec 2019 16:51:47 +0100 Subject: [PATCH 05/15] json-rpc: Don't let users send messages that are handled internally We cannot let users use `sendcustommsg` to inject messages that are handled internally since it could result in our internal state tracking being borked. --- lightningd/peer_control.c | 12 ++++++++++++ tools/gen/header_template | 9 +++++++++ tools/gen/impl_template | 12 ++++++++++++ 3 files changed, 33 insertions(+) diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index d56ca62077b3..d674ba5f19f4 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -2385,6 +2385,7 @@ static struct command_result *json_sendcustommsg(struct command *cmd, struct peer *peer; struct subd *owner; u8 *msg; + int type; if (!param(cmd, buffer, params, p_req("node_id", param_node_id, &dest), @@ -2392,6 +2393,17 @@ static struct command_result *json_sendcustommsg(struct command *cmd, NULL)) return command_param_failed(); + type = fromwire_peektype(msg); + if (wire_type_is_defined(type)) { + return command_fail( + cmd, JSONRPC2_INVALID_REQUEST, + "Cannot send messages of type %d (%s). It is not possible " + "to send messages that have a type managed internally " + "since that might cause issues with the internal state " + "tracking.", + type, wire_type_name(type)); + } + peer = peer_by_id(cmd->ld, dest); if (!peer) { return command_fail(cmd, JSONRPC2_INVALID_REQUEST, diff --git a/tools/gen/header_template b/tools/gen/header_template index c7e816d93e28..6175dcd5ae2a 100644 --- a/tools/gen/header_template +++ b/tools/gen/header_template @@ -26,6 +26,15 @@ enum ${enum_set['name']} { ## The 'name' functions for the enums % for enum_set in enum_sets: const char *${enum_set['name']}_name(int e); + +/** + * Determine whether a given message type is defined as a message. + * + * Returns true if the message type is part of the message definitions we have + * generated parsers for, false if it is a custom message that cannot be + * handled internally. + */ +bool ${enum_set['name']}_is_defined(u16 type); % endfor ## Structs for subtypes + tlv messages diff --git a/tools/gen/impl_template b/tools/gen/impl_template index 118d57a263a6..1c4f2e231f50 100644 --- a/tools/gen/impl_template +++ b/tools/gen/impl_template @@ -31,6 +31,18 @@ const char *${enum_set['name']}_name(int e) snprintf(invalidbuf, sizeof(invalidbuf), "INVALID %i", e); return invalidbuf; } + +bool ${enum_set['name']}_is_defined(u16 type) +{ + switch ((enum ${enum_set['name']})type) { + % for msg in enum_set['set']: + case ${msg.enum_name()}:; + % endfor + return true; + } + return false; +} + % endfor ## START PARTIALS ## Subtype and TLV-msg towire_ From 2670c8978a8f42e9d99a8f74901075b16da10440 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 4 Dec 2019 16:53:12 +0100 Subject: [PATCH 06/15] openingd: Implement sendcustommsg handling in openingd Most of the work is done in `lightningd`, here we just need to queue the message itself. --- openingd/openingd.c | 10 ++++++++- tests/test_misc.py | 44 ++++++++++++++++++++++++++++++++++++++++ wallet/test/run-wallet.c | 6 ++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/openingd/openingd.c b/openingd/openingd.c index 380d56a22878..4c86a6405e25 100644 --- a/openingd/openingd.c +++ b/openingd/openingd.c @@ -1355,6 +1355,14 @@ static void handle_dev_memleak(struct state *state, const u8 *msg) take(towire_opening_dev_memleak_reply(NULL, found_leak))); } + +/* We were told to send a custommsg to the peer by `lightningd`. All the + * verification is done on the side of `lightningd` so we should be good to + * just forward it here. */ +static void openingd_send_custommsg(struct state *state, const u8 *msg) +{ + sync_crypto_write(state->pps, take(msg)); +} #endif /* DEVELOPER */ /* Standard lightningd-fd-is-ready-to-read demux code. Again, we could hang @@ -1418,7 +1426,7 @@ static u8 *handle_master_in(struct state *state) switch ((enum common_wire_type)t) { #if DEVELOPER case WIRE_CUSTOMMSG_OUT: - /* TODO(cdecker) Add handling of custom messages. */ + openingd_send_custommsg(state, msg); return NULL; #else case WIRE_CUSTOMMSG_OUT: diff --git a/tests/test_misc.py b/tests/test_misc.py index e10e7cac0db0..dee1331f5580 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -2060,3 +2060,47 @@ def test_waitblockheight(node_factory, executor, bitcoind): bitcoind.generate_block(1) sync_blockheight(bitcoind, [node]) fut2.result(5) + + +@unittest.skipIf(not DEVELOPER, "Needs dev-sendcustommsg") +def test_sendcustommsg(node_factory): + """Check that we can send custommsgs to peers in various states. + + `l2` is the node under test. `l1` has a channel with `l2` and should + therefore be attached to `channeld`. `l4` is just connected, so it should + be attached to `openingd`. `l3` has a channel open, but is disconnected + and we can't send to it. + + """ + l1, l2, l3 = node_factory.line_graph(3, opts={'log-level': 'io'}) + l4 = node_factory.get_node(options={'log-level': 'io'}) + l2.connect(l4) + l3.stop() + msg = r'ff' * 32 + serialized = r'04070020' + msg + + # This address doesn't exist so we should get an error when we try sending + # a message to it. + node_id = '02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f' + with pytest.raises(RpcError, match=r'No such peer'): + l1.rpc.dev_sendcustommsg(node_id, msg) + + # `l3` is disconnected and we can't send messages to it + assert(not l2.rpc.listpeers(l3.info['id'])['peers'][0]['connected']) + with pytest.raises(RpcError, match=r'Peer is not connected'): + l2.rpc.dev_sendcustommsg(l3.info['id'], msg) + + # We should not be able to send a bogus `ping` message, since it collides + # with a message defined in the spec, and could potentially mess up our + # internal state. + with pytest.raises(RpcError, match=r'Cannot send messages of type 18 .WIRE_PING.'): + l2.rpc.dev_sendcustommsg(l2.info['id'], r'0012') + + l2.rpc.dev_sendcustommsg(l1.info['id'], msg) + l2.rpc.dev_sendcustommsg(l4.info['id'], msg) + l2.daemon.wait_for_log( + r'{peer_id}-openingd-chan#[0-9]: \[OUT\] {serialized}'.format( + serialized=serialized, peer_id=l4.info['id'] + ) + ) + l4.daemon.wait_for_log(r'\[IN\] {}'.format(serialized)) diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 4709f4b1cbba..4c0090a846fd 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -640,6 +640,12 @@ struct txowatch *watch_txo(const tal_t *ctx UNNEEDED, size_t input_num UNNEEDED, const struct block *block)) { fprintf(stderr, "watch_txo called!\n"); abort(); } +/* Generated stub for wire_type_is_defined */ +bool wire_type_is_defined(u16 type UNNEEDED) +{ fprintf(stderr, "wire_type_is_defined called!\n"); abort(); } +/* Generated stub for wire_type_name */ +const char *wire_type_name(int e UNNEEDED) +{ fprintf(stderr, "wire_type_name called!\n"); abort(); } /* AUTOGENERATED MOCKS END */ #if DEVELOPER From 7224ca76dbeb77fcefcbe17e443de0c847e027e2 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 4 Dec 2019 17:03:49 +0100 Subject: [PATCH 07/15] channeld: Send messages if instructed to by lightningd --- channeld/channeld.c | 10 +++++++++- tests/test_misc.py | 16 ++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/channeld/channeld.c b/channeld/channeld.c index 6506ae0dbea4..031fe88a73b7 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -2852,6 +2852,14 @@ static void handle_dev_memleak(struct peer *peer, const u8 *msg) take(towire_channel_dev_memleak_reply(NULL, found_leak))); } + +/* We were told to send a custommsg to the peer by `lightningd`. All the + * verification is done on the side of `lightningd` so we should be good to + * just forward it here. */ +static void channeld_send_custommsg(struct peer *peer, const u8 *msg) +{ + sync_crypto_write(peer->pps, take(msg)); +} #endif /* DEVELOPER */ static void req_in(struct peer *peer, const u8 *msg) @@ -2917,7 +2925,7 @@ static void req_in(struct peer *peer, const u8 *msg) switch ((enum common_wire_type)t) { #if DEVELOPER case WIRE_CUSTOMMSG_OUT: - /* TODO(cdecker) Add handling of custom messages. */ + channeld_send_custommsg(peer, msg); return; #else case WIRE_CUSTOMMSG_OUT: diff --git a/tests/test_misc.py b/tests/test_misc.py index dee1331f5580..648374fa7ed4 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -2096,11 +2096,23 @@ def test_sendcustommsg(node_factory): with pytest.raises(RpcError, match=r'Cannot send messages of type 18 .WIRE_PING.'): l2.rpc.dev_sendcustommsg(l2.info['id'], r'0012') + # This should work since the peer is currently owned by `channeld` l2.rpc.dev_sendcustommsg(l1.info['id'], msg) + l2.daemon.wait_for_log( + r'{peer_id}-{owner}-chan#[0-9]: \[OUT\] {serialized}'.format( + owner='channeld', serialized=serialized, peer_id=l1.info['id'] + ) + ) + l1.daemon.wait_for_log(r'\[IN\] {}'.format(serialized)) + l1.daemon.wait_for_log( + r'Got a custom message {serialized} from peer {peer_id}'.format( + serialized=serialized, peer_id=l2.info['id'])) + + # This should work since the peer is currently owned by `openingd` l2.rpc.dev_sendcustommsg(l4.info['id'], msg) l2.daemon.wait_for_log( - r'{peer_id}-openingd-chan#[0-9]: \[OUT\] {serialized}'.format( - serialized=serialized, peer_id=l4.info['id'] + r'{peer_id}-{owner}-chan#[0-9]: \[OUT\] {serialized}'.format( + owner='openingd', serialized=serialized, peer_id=l4.info['id'] ) ) l4.daemon.wait_for_log(r'\[IN\] {}'.format(serialized)) From e8f47bf59a97a7f4cbef8125618c4030dbb7ca44 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 4 Dec 2019 22:58:42 +0100 Subject: [PATCH 08/15] channeld: Forward custommsgs to lightningd --- channeld/channeld.c | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/channeld/channeld.c b/channeld/channeld.c index 031fe88a73b7..8d083e3552ca 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -1818,6 +1818,27 @@ static void handle_peer_shutdown(struct peer *peer, const u8 *shutdown) billboard_update(peer); } +/* Try to handle a custommsg Returns true if it was a custom message and has + * been handled, false if the message was not handled. + */ +static bool channeld_handle_custommsg(const u8 *msg) +{ +#if DEVELOPER + enum wire_type type = fromwire_peektype(msg); + if (type % 2 == 1 && !wire_type_is_defined(type)) { + /* The message is not part of the messages we know how to + * handle. Assuming this is a custommsg, we just forward it to the + * master. */ + wire_sync_write(MASTER_FD, take(towire_custommsg_in(NULL, msg))); + return true; + } else { + return false; + } +#else + return false; +#endif +} + static void peer_in(struct peer *peer, const u8 *msg) { enum wire_type type = fromwire_peektype(msg); @@ -1830,6 +1851,9 @@ static void peer_in(struct peer *peer, const u8 *msg) return; } + if (channeld_handle_custommsg(msg)) + return; + /* Since LND seems to send errors which aren't actually fatal events, * we treat errors here as soft. */ if (handle_peer_gossip_or_error(peer->pps, &peer->channel_id, true, msg)) @@ -2330,9 +2354,10 @@ static void peer_reconnect(struct peer *peer, do { clean_tmpctx(); msg = sync_crypto_read(tmpctx, peer->pps); - } while (handle_peer_gossip_or_error(peer->pps, &peer->channel_id, true, - msg) - || capture_premature_msg(&premature_msgs, msg)); + } while (channeld_handle_custommsg(msg) || + handle_peer_gossip_or_error(peer->pps, &peer->channel_id, true, + msg) || + capture_premature_msg(&premature_msgs, msg)); if (peer->channel->option_static_remotekey) { struct pubkey ignore; From 032ff807e7081a8d80c32753a9054d967e5d14e6 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 4 Dec 2019 22:59:23 +0100 Subject: [PATCH 09/15] openingd: Forward custommsgs to lightningd --- openingd/openingd.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openingd/openingd.c b/openingd/openingd.c index 4c86a6405e25..82004735613d 100644 --- a/openingd/openingd.c +++ b/openingd/openingd.c @@ -1282,6 +1282,18 @@ static u8 *handle_peer_in(struct state *state) if (t == WIRE_OPEN_CHANNEL) return fundee_channel(state, msg); +#if DEVELOPER + /* Handle custommsgs */ + enum wire_type type = fromwire_peektype(msg); + if (type % 2 == 1 && !wire_type_is_defined(type)) { + /* The message is not part of the messages we know how to + * handle. Assuming this is a custommsg, we just forward it to the + * master. */ + wire_sync_write(REQ_FD, take(towire_custommsg_in(NULL, msg))); + return NULL; + } +#endif + /* Handles standard cases, and legal unknown ones. */ if (handle_peer_gossip_or_error(state->pps, &state->channel_id, false, msg)) From 2228953e946b36e81e2d685689a26a9a8af70343 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 4 Dec 2019 23:01:20 +0100 Subject: [PATCH 10/15] json-rpc: Restrict custommsgs to be odd-typed This solves a couple of issues with the need to synchronously drop the connection in case we were required to understand what the peer was talking about while still allowing users to experiment, just not kill connections. --- lightningd/peer_control.c | 10 ++++++++++ tests/test_misc.py | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index d674ba5f19f4..3de82c74a61e 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -2404,6 +2404,16 @@ static struct command_result *json_sendcustommsg(struct command *cmd, type, wire_type_name(type)); } + if (type % 2 == 0) { + return command_fail( + cmd, JSONRPC2_INVALID_REQUEST, + "Cannot send even-typed %d custom message. Currently " + "custom messages are limited to odd-numbered message " + "types, as even-numbered types might result in " + "disconnections.", + type); + } + peer = peer_by_id(cmd->ld, dest); if (!peer) { return command_fail(cmd, JSONRPC2_INVALID_REQUEST, diff --git a/tests/test_misc.py b/tests/test_misc.py index 648374fa7ed4..fe2bd5a6546d 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -2096,6 +2096,12 @@ def test_sendcustommsg(node_factory): with pytest.raises(RpcError, match=r'Cannot send messages of type 18 .WIRE_PING.'): l2.rpc.dev_sendcustommsg(l2.info['id'], r'0012') + # The sendcustommsg RPC call is currently limited to odd-typed messages, + # since they will not result in disconnections or even worse channel + # failures. + with pytest.raises(RpcError, match=r'Cannot send even-typed [0-9]+ custom message'): + l2.rpc.dev_sendcustommsg(l2.info['id'], r'00FE') + # This should work since the peer is currently owned by `channeld` l2.rpc.dev_sendcustommsg(l1.info['id'], msg) l2.daemon.wait_for_log( From 31757a8b61a9a7fe41b0426c697eb6dc0d065b35 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 4 Dec 2019 23:02:54 +0100 Subject: [PATCH 11/15] common: Add an assertion for custommsgs in gossip handler This is mainly meant as a marker so that we can later remove the code if we decide to make the handling of custommsgs a non-developer option. It marks the place that we would otherwise handle what in dev-mode is a custommsg. --- common/read_peer_msg.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/common/read_peer_msg.c b/common/read_peer_msg.c index f6c74e1dfbcb..da52b903e55b 100644 --- a/common/read_peer_msg.c +++ b/common/read_peer_msg.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -155,6 +156,11 @@ bool handle_peer_gossip_or_error(struct per_peer_state *pps, bool all_channels; struct channel_id actual; +#if DEVELOPER + /* Any odd-typed unknown message is handled by the caller, so if we + * find one here it's an error. */ + assert(!is_unknown_msg_discardable(msg)); +#else /* BOLT #1: * * A receiving node: @@ -163,6 +169,7 @@ bool handle_peer_gossip_or_error(struct per_peer_state *pps, */ if (is_unknown_msg_discardable(msg)) goto handled; +#endif if (handle_timestamp_filter(pps, msg)) return true; From 5cb42859823260a5a5868c57dfff207ac99e8437 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Thu, 5 Dec 2019 00:11:28 +0100 Subject: [PATCH 12/15] peer: Add custommsg hook and wire it into channeld and openingd --- lightningd/channel_control.c | 2 +- lightningd/opening_control.c | 2 +- lightningd/peer_control.c | 39 ++++++++++++++++++++++++++++++++++++ lightningd/peer_control.h | 2 ++ wallet/test/run-wallet.c | 3 +++ 5 files changed, 46 insertions(+), 2 deletions(-) diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index 9256151bf99e..1cb3e84e4c98 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -323,7 +323,7 @@ static unsigned channel_msg(struct subd *sd, const u8 *msg, const int *fds) switch ((enum common_wire_type)t) { #if DEVELOPER case WIRE_CUSTOMMSG_IN: - /* TODO(cdecker) Add handling of custom messages. */ + handle_custommsg_in(sd->ld, sd->node_id, msg); break; #else case WIRE_CUSTOMMSG_IN: diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c index 66eaed5f84cd..810a45fe57d9 100644 --- a/lightningd/opening_control.c +++ b/lightningd/opening_control.c @@ -926,7 +926,7 @@ static unsigned int openingd_msg(struct subd *openingd, switch ((enum common_wire_type)t) { #if DEVELOPER case WIRE_CUSTOMMSG_IN: - /* TODO(cdecker) Add handling of custom messages. */ + handle_custommsg_in(openingd->ld, openingd->node_id, msg); return 0; #else case WIRE_CUSTOMMSG_IN: diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index 3de82c74a61e..bda3ff4741e8 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -2375,6 +2375,45 @@ void peer_dev_memleak(struct command *cmd) peer_memleak_req_next(cmd, NULL); } +struct custommsg_payload { + struct node_id peer_id; + const u8 *msg; +}; + +static void custommsg_callback(struct custommsg_payload *payload, + const char *buffer, const jsmntok_t *toks) +{ + tal_free(payload); +} + +static void custommsg_payload_serialize(struct custommsg_payload *payload, + struct json_stream *stream) +{ + json_add_hex_talarr(stream, "message", payload->msg); + json_add_node_id(stream, "peer_id", &payload->peer_id); +} + +REGISTER_PLUGIN_HOOK(custommsg, custommsg_callback, struct custommsg_payload *, + custommsg_payload_serialize, struct custommsg_payload *); + +void handle_custommsg_in(struct lightningd *ld, const struct node_id *peer_id, + const u8 *msg) +{ + struct custommsg_payload *p = tal(NULL, struct custommsg_payload); + u8 *custommsg; + + if (!fromwire_custommsg_in(NULL, msg, &custommsg)) { + log_broken(ld->log, "Malformed custommsg from peer %s: %s", + type_to_string(tmpctx, struct node_id, peer_id), + tal_hex(tmpctx, msg)); + return; + } + + p->peer_id = *peer_id; + p->msg = tal_steal(p, custommsg); + plugin_hook_call_custommsg(ld, p, p); +} + static struct command_result *json_sendcustommsg(struct command *cmd, const char *buffer, const jsmntok_t *obj UNNEEDED, diff --git a/lightningd/peer_control.h b/lightningd/peer_control.h index 100756699027..0d53760a9c9a 100644 --- a/lightningd/peer_control.h +++ b/lightningd/peer_control.h @@ -93,6 +93,8 @@ struct htlc_in_map *load_channels_from_wallet(struct lightningd *ld); #if DEVELOPER void peer_dev_memleak(struct command *cmd); +void handle_custommsg_in(struct lightningd *ld, const struct node_id *peer_id, + const u8 *msg); #endif /* DEVELOPER */ /* Triggered at each new block. */ diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 4c0090a846fd..b6f5fcbef58c 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -111,6 +111,9 @@ bool fromwire_channel_sending_commitsig(const tal_t *ctx UNNEEDED, const void *p /* Generated stub for fromwire_connect_peer_connected */ bool fromwire_connect_peer_connected(const tal_t *ctx UNNEEDED, const void *p UNNEEDED, struct node_id *id UNNEEDED, struct wireaddr_internal *addr UNNEEDED, struct per_peer_state **pps UNNEEDED, u8 **features UNNEEDED) { fprintf(stderr, "fromwire_connect_peer_connected called!\n"); abort(); } +/* Generated stub for fromwire_custommsg_in */ +bool fromwire_custommsg_in(const tal_t *ctx UNNEEDED, const void *p UNNEEDED, u8 **msg UNNEEDED) +{ fprintf(stderr, "fromwire_custommsg_in called!\n"); abort(); } /* Generated stub for fromwire_gossip_get_channel_peer_reply */ bool fromwire_gossip_get_channel_peer_reply(const tal_t *ctx UNNEEDED, const void *p UNNEEDED, struct node_id **peer_id UNNEEDED) { fprintf(stderr, "fromwire_gossip_get_channel_peer_reply called!\n"); abort(); } From 0e9b7fd9e37d7042ac0af219a17457c4a7f9c762 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Thu, 5 Dec 2019 00:16:11 +0100 Subject: [PATCH 13/15] pytest: Add a plugin for custommsgs and check that they get the msgs This completes the custommsg epic, finally we are back where we began all that time ago (about 4 hours really...): in a plugin that implements some custom logic. --- tests/plugins/custommsg.py | 15 +++++++++++++++ tests/test_misc.py | 9 +++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100755 tests/plugins/custommsg.py diff --git a/tests/plugins/custommsg.py b/tests/plugins/custommsg.py new file mode 100755 index 000000000000..3c99b03660f0 --- /dev/null +++ b/tests/plugins/custommsg.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +from pyln.client import Plugin + +plugin = Plugin() + + +@plugin.hook('custommsg') +def on_custommsg(peer_id, message, plugin, **kwargs): + plugin.log("Got a custom message {msg} from peer {peer_id}".format( + msg=message, + peer_id=peer_id + )) + + +plugin.run() diff --git a/tests/test_misc.py b/tests/test_misc.py index fe2bd5a6546d..e55c9dddbd46 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -2072,8 +2072,10 @@ def test_sendcustommsg(node_factory): and we can't send to it. """ - l1, l2, l3 = node_factory.line_graph(3, opts={'log-level': 'io'}) - l4 = node_factory.get_node(options={'log-level': 'io'}) + plugin = os.path.join(os.path.dirname(__file__), "plugins", "custommsg.py") + opts = {'log-level': 'io', 'plugin': plugin} + l1, l2, l3 = node_factory.line_graph(3, opts=opts) + l4 = node_factory.get_node(options=opts) l2.connect(l4) l3.stop() msg = r'ff' * 32 @@ -2122,3 +2124,6 @@ def test_sendcustommsg(node_factory): ) ) l4.daemon.wait_for_log(r'\[IN\] {}'.format(serialized)) + l4.daemon.wait_for_log( + r'Got a custom message {serialized} from peer {peer_id}'.format( + serialized=serialized, peer_id=l2.info['id'])) From 8fabee09aa31a0e4f814af7c8bdcdf0e6e110746 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Mon, 9 Dec 2019 16:32:49 +0100 Subject: [PATCH 14/15] docs: Add manpage for dev-sendcustommsg It's a dev-* command for now, but better document it so people can use it rather than having them guess how it's supposed to work. --- doc/Makefile | 1 + doc/index.rst | 1 + doc/lightning-dev-sendcustommsg.7 | 0 doc/lightning-dev-sendcustommsg.7.md | 63 ++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+) create mode 100644 doc/lightning-dev-sendcustommsg.7 create mode 100644 doc/lightning-dev-sendcustommsg.7.md diff --git a/doc/Makefile b/doc/Makefile index db2b300293a7..dc5738e59a77 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -16,6 +16,7 @@ MANPAGES := doc/lightning-cli.1 \ doc/lightning-decodepay.7 \ doc/lightning-delexpiredinvoice.7 \ doc/lightning-delinvoice.7 \ + doc/lightning-dev-sendcustommsg.7 \ doc/lightning-disconnect.7 \ doc/lightning-fundchannel.7 \ doc/lightning-fundchannel_start.7 \ diff --git a/doc/index.rst b/doc/index.rst index e6380274fee3..325a02607e59 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -39,6 +39,7 @@ c-lightning Documentation lightning-decodepay lightning-delexpiredinvoice lightning-delinvoice + lightning-dev-sendcustommsg lightning-disconnect lightning-fundchannel lightning-fundchannel_cancel diff --git a/doc/lightning-dev-sendcustommsg.7 b/doc/lightning-dev-sendcustommsg.7 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/doc/lightning-dev-sendcustommsg.7.md b/doc/lightning-dev-sendcustommsg.7.md new file mode 100644 index 000000000000..dfcbc17b5e2e --- /dev/null +++ b/doc/lightning-dev-sendcustommsg.7.md @@ -0,0 +1,63 @@ +lightning-dev-sendcustommsg -- Low-level interface to send protocol messages to peers +===================================================================================== + +SYNOPSIS +-------- + +**dev-sendcustommsg** *node_id* *msg* + +DESCRIPTION +----------- + +The `dev-sendcustommsg` RPC method allows the user to inject a custom message +into the communication with the peer with the given `node_id`. This is +intended as a low-level interface to implement custom protocol extensions on +top, not for direct use by end-users. + +The message must be a hex encoded well-formed message, including the 2-byte +type prefix, but excluding the length prefix which will be added by the RPC +method. The messages must not use even-numbered types, since these may require +synchronous handling on the receiving side, and can cause the connection to be +dropped. The message types may also not use one of the internally handled +types, since that may cause issues with the internal state tracking of +c-lightning. + +The node specified by `node_id` must be a peer, i.e., it must have a direct +connection with the node receiving the RPC call, and the connection must be +established. For a method to send arbitrary messages over multiple hops, +including hops that do not understand the custom message, see the +`createonion` and `sendonion` RPC methods. Messages can only be injected if +the connection is handled by `openingd` or `channeld`. Messages cannot be +injected when the peer is handled by `onchaind` or `closingd` since these do +not have a connection, or are synchronous daemons that do not handle +spontaneous messages. + +On the reveiving end a plugin may implement the `custommsg` plugin hook and +get notified about incoming messages. + +RETURN VALUE +------------ + +The method will validate the arguments and queue the message for delivery +through the daemon that is currently handling the connection. Queuing provides +best effort guarantees and the message may not be delivered if the connection +is terminated while the message is queued. The RPC method will return as soon +as the message is queued. + +If any of the above limitations is not respected the method returns an +explicit error message stating the issue. + +AUTHOR +------ + +Christian Decker <> is mainly responsible. + +SEE ALSO +-------- + +lightning-createonion(7), lightning-sendonion(7) + +RESOURCES +--------- + +Main web site: From 09c72dddf9f5d872e5692cc2d005e6c860f16e40 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Mon, 9 Dec 2019 16:52:44 +0100 Subject: [PATCH 15/15] doc: Add a section about the `custommsg` hook in the plugin doc --- doc/PLUGINS.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index 5a987da8af15..1458c0b4ef35 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -760,8 +760,43 @@ Return a custom error to the request sender: } ``` + +#### `custommsg` + +The `custommsg` plugin hook is the receiving counterpart to the +[`dev-sendcustommsg`][sendcustommsg] RPC method and allows plugins to handle +messages that are not handled internally. The goal of these two components is +to allow the implementation of custom protocols or prototypes on top of a +c-lightning node, without having to change the node's implementation itself. + +The payload for a call follows this format: + +```json +{ + "peer_id": "02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f", + "message": "1337ffffffff" +} +``` + +This payload would have been sent by the peer with the `node_id` matching +`peer_id`, and the message has type `0x1337` and contents `ffffffff`. Notice +that the messages are currently limited to odd-numbered types and must not +match a type that is handled internally by c-lightning. These limitations are +in place in order to avoid conflicts with the internal state tracking, and +avoiding disconnections or channel closures, since odd-numbered message can be +ignored by nodes (see ["it's ok to be odd" in the specification][oddok] for +details). The plugin must implement the parsing of the message, including the +type prefix, since c-lightning does not know how to parse the message. + +The result for this hook is currently being discarded. For future uses of the +result we suggest just returning a `null`. This will ensure backward +compatibility should the semantics be changed in future. + + [jsonrpc-spec]: https://www.jsonrpc.org/specification [jsonrpc-notification-spec]: https://www.jsonrpc.org/specification#notification [bolt4]: https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md [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 +[sendcustommsg]: lightning-dev-sendcustommsg.7.html +[oddok]: https://github.com/lightningnetwork/lightning-rfc/blob/master/00-introduction.md#its-ok-to-be-odd