diff --git a/CHANGELOG.md b/CHANGELOG.md index 69584c950..41027b66d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ This release named by @molxyz and [@ctrlbreak](https://twitter.com/ctrlbreak). - JSON API: use `\n\n` to terminate responses, for simplified parsing (pylightning now relies on this) - JSON API: `fundchannel` now includes an `announce` option, when false it will keep channel private. Defaults to true. - JSON API: `listpeers`'s `channels` now includes a `private` flag to indicate if channel is announced or not. -- JSON API: `invoice` route hints may now include private channels if you have no public ones. +- JSON API: `invoice` route hints may now include private channels if you have no public ones, unless new option `exposeprivatechannels` is false. - Plugins: experimental plugin support for `lightningd`, including option passthrough and JSON-RPC passthrough. ### Changed diff --git a/contrib/pylightning/lightning/lightning.py b/contrib/pylightning/lightning/lightning.py index 18466ee77..f7adc37a6 100644 --- a/contrib/pylightning/lightning/lightning.py +++ b/contrib/pylightning/lightning/lightning.py @@ -175,7 +175,7 @@ class LightningRpc(UnixDomainSocketRpc): } return self.call("listchannels", payload) - def invoice(self, msatoshi, label, description, expiry=None, fallbacks=None, preimage=None): + def invoice(self, msatoshi, label, description, expiry=None, fallbacks=None, preimage=None, exposeprivatechannels=None): """ Create an invoice for {msatoshi} with {label} and {description} with optional {expiry} seconds (default 1 hour) @@ -186,7 +186,8 @@ class LightningRpc(UnixDomainSocketRpc): "description": description, "expiry": expiry, "fallbacks": fallbacks, - "preimage": preimage + "preimage": preimage, + "exposeprivatechannels": exposeprivatechannels } return self.call("invoice", payload) diff --git a/doc/lightning-invoice.7 b/doc/lightning-invoice.7 index f0e6cc036..3c77cabeb 100644 --- a/doc/lightning-invoice.7 +++ b/doc/lightning-invoice.7 @@ -2,12 +2,12 @@ .\" Title: lightning-invoice .\" Author: [see the "AUTHOR" section] .\" Generator: DocBook XSL Stylesheets v1.79.1 -.\" Date: 09/27/2018 +.\" Date: 12/17/2018 .\" Manual: \ \& .\" Source: \ \& .\" Language: English .\" -.TH "LIGHTNING\-INVOICE" "7" "09/27/2018" "\ \&" "\ \&" +.TH "LIGHTNING\-INVOICE" "7" "12/17/2018" "\ \&" "\ \&" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- @@ -31,10 +31,10 @@ lightning-invoice \- Command for accepting payments\&. .SH "SYNOPSIS" .sp -\fBinvoice\fR \fImsatoshi\fR \fIlabel\fR \fIdescription\fR [\fIexpiry\fR] [\fIfallbacks\fR] [\fIpreimage\fR] +\fBinvoice\fR \fImsatoshi\fR \fIlabel\fR \fIdescription\fR [\fIexpiry\fR] [\fIfallbacks\fR] [\fIpreimage\fR] [\fIexposeprivatechannels\fR] .SH "DESCRIPTION" .sp -The \fBinvoice\fR RPC command creates the expectation of a payment of a given amount of milli\-satoshi: it returns a unique token which another lightning daemon can use to pay this invoice\&. +The \fBinvoice\fR RPC command creates the expectation of a payment of a given amount of milli\-satoshi: it returns a unique token which another lightning daemon can use to pay this invoice\&. This token includes a \fIroute hint\fR description of an incoming channel with capacity to pay the invoice, if any exists\&. .sp The \fImsatoshi\fR can be the string "any", which creates an invoice that can be paid with any amount\&. .sp @@ -47,6 +47,8 @@ The \fIexpiry\fR is optionally the number of seconds the invoice is valid for\&. The \fIfallbacks\fR array is one or more fallback addresses to include in the invoice (in order from most\-preferred to least): note that these arrays are not currently tracked to fulfill the invoice\&. .sp The \fIpreimage\fR is a 64\-digit hex string to be used as payment preimage for the created invoice\&. By default, if unspecified, lightningd will generate a secure pseudorandom preimage seeded from an appropriate entropy source on your system\&. \fBIMPORTANT\fR: if you specify the \fIpreimage\fR, you are responsible, to ensure appropriate care for generating using a secure pseudorandom generator seeded with sufficient entropy, and keeping the preimage secret\&. This parameter is an advanced feature intended for use with cutting\-edge cryptographic protocols and should not be used unless explicitly needed\&. +.sp +The \fIexposeprivatechannels\fR includes unpublished channels in the selection of channels for the route hint; by default they are excluded\&. .SH "RETURN VALUE" .sp On success, a hash is returned as \fIpayment_hash\fR to be given to the payer, and the \fIexpiry_time\fR as a UNIX timestamp\&. It also returns a BOLT11 invoice as \fIbolt11\fR to be given to the payer\&. diff --git a/doc/lightning-invoice.7.txt b/doc/lightning-invoice.7.txt index 1013dc346..2c0e52856 100644 --- a/doc/lightning-invoice.7.txt +++ b/doc/lightning-invoice.7.txt @@ -8,13 +8,15 @@ lightning-invoice - Command for accepting payments. SYNOPSIS -------- -*invoice* 'msatoshi' 'label' 'description' ['expiry'] ['fallbacks'] ['preimage'] +*invoice* 'msatoshi' 'label' 'description' ['expiry'] ['fallbacks'] ['preimage'] ['exposeprivatechannels'] DESCRIPTION ----------- The *invoice* RPC command creates the expectation of a payment of a given amount of milli-satoshi: it returns a unique token which another -lightning daemon can use to pay this invoice. +lightning daemon can use to pay this invoice. This token includes a +'route hint' description of an incoming channel with capacity to pay +the invoice, if any exists. The 'msatoshi' can be the string "any", which creates an invoice that can be paid with any amount. @@ -48,6 +50,11 @@ secret. This parameter is an advanced feature intended for use with cutting-edge cryptographic protocols and should not be used unless explicitly needed. +If specified, 'exposeprivatechannels' overrides the default route hint +logic, which will use unpublished channels only if there are no +published channels. If 'true' unpublished channels are always +considered as a route hint candidate; if 'false', never. + RETURN VALUE ------------ diff --git a/gossipd/gossip_wire.csv b/gossipd/gossip_wire.csv index a82c00902..b6641c195 100644 --- a/gossipd/gossip_wire.csv +++ b/gossipd/gossip_wire.csv @@ -144,6 +144,7 @@ gossip_dev_memleak_reply,,leak,bool # master -> gossipd: get route_info for our incoming channels gossip_get_incoming_channels,3025 +gossip_get_incoming_channels,,private_too,?bool # gossipd -> master: here they are. gossip_get_incoming_channels_reply,3125 diff --git a/gossipd/gossipd.c b/gossipd/gossipd.c index 5cc653924..a00b3868b 100644 --- a/gossipd/gossipd.c +++ b/gossipd/gossipd.c @@ -2119,6 +2119,18 @@ static bool node_has_public_channels(const struct node *peer, return false; } +/*~ The `exposeprivate` flag is a trinary: NULL == dynamic, otherwise + * value decides. Thus, we provide two wrappers for clarity: */ +static bool never_expose(bool *exposeprivate) +{ + return exposeprivate && !*exposeprivate; +} + +static bool always_expose(bool *exposeprivate) +{ + return exposeprivate && *exposeprivate; +} + /*~ For routeboost, we offer payers a hint of what incoming channels might * have capacity for their payment. To do this, lightningd asks for the * information about all channels to this node; but gossipd doesn't know about @@ -2130,11 +2142,20 @@ static struct io_plan *get_incoming_channels(struct io_conn *conn, struct node *node; struct route_info *public = tal_arr(tmpctx, struct route_info, 0); struct route_info *private = tal_arr(tmpctx, struct route_info, 0); - bool has_public = false; + bool has_public; + bool *exposeprivate; - if (!fromwire_gossip_get_incoming_channels(msg)) + if (!fromwire_gossip_get_incoming_channels(tmpctx, msg, &exposeprivate)) master_badmsg(WIRE_GOSSIP_GET_INCOMING_CHANNELS, msg); + status_trace("exposeprivate = %s", + exposeprivate ? (*exposeprivate ? "TRUE" : "FALSE") : "NULL"); + status_trace("msg = %s", tal_hex(tmpctx, msg)); + status_trace("always_expose = %u, never_expose = %u", + always_expose(exposeprivate), never_expose(exposeprivate)); + + has_public = always_expose(exposeprivate); + node = get_node(daemon->rstate, &daemon->rstate->local_id); if (node) { for (size_t i = 0; i < tal_count(node->chans); i++) { @@ -2160,7 +2181,7 @@ static struct io_plan *get_incoming_channels(struct io_conn *conn, if (!node_has_public_channels(other_node(node, c), c)) continue; - if (is_chan_public(c)) + if (always_expose(exposeprivate) || is_chan_public(c)) tal_arr_expand(&public, ri); else tal_arr_expand(&private, ri); @@ -2168,7 +2189,7 @@ static struct io_plan *get_incoming_channels(struct io_conn *conn, } /* If no public channels (even deadend ones!), share private ones. */ - if (!has_public) + if (!has_public && !never_expose(exposeprivate)) msg = towire_gossip_get_incoming_channels_reply(NULL, private); else msg = towire_gossip_get_incoming_channels_reply(NULL, public); diff --git a/lightningd/invoice.c b/lightningd/invoice.c index 179bb7f92..4eba1fcac 100644 --- a/lightningd/invoice.c +++ b/lightningd/invoice.c @@ -308,6 +308,7 @@ static struct command_result *json_invoice(struct command *cmd, const u8 **fallback_scripts = NULL; u64 *expiry; struct sha256 rhash; + bool *exposeprivate; info = tal(cmd, struct invoice_info); info->cmd = cmd; @@ -319,6 +320,7 @@ static struct command_result *json_invoice(struct command *cmd, p_opt_def("expiry", param_u64, &expiry, 3600), p_opt("fallbacks", param_array, &fallbacks), p_opt("preimage", param_tok, &preimagetok), + p_opt("exposeprivatechannels", param_bool, &exposeprivate), NULL)) return command_param_failed(); @@ -381,8 +383,10 @@ static struct command_result *json_invoice(struct command *cmd, if (fallback_scripts) info->b11->fallbacks = tal_steal(info->b11, fallback_scripts); + log_debug(cmd->ld->log, "exposeprivate = %s", + exposeprivate ? (*exposeprivate ? "TRUE" : "FALSE") : "NULL"); subd_req(cmd, cmd->ld->gossip, - take(towire_gossip_get_incoming_channels(NULL)), + take(towire_gossip_get_incoming_channels(NULL, exposeprivate)), -1, 0, gossipd_incoming_channels_reply, info); return command_still_pending(cmd); diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index f5da0dbe8..385d9f791 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -397,7 +397,7 @@ u8 *towire_errorfmt(const tal_t *ctx UNNEEDED, const char *fmt UNNEEDED, ...) { fprintf(stderr, "towire_errorfmt called!\n"); abort(); } /* Generated stub for towire_gossip_get_incoming_channels */ -u8 *towire_gossip_get_incoming_channels(const tal_t *ctx UNNEEDED) +u8 *towire_gossip_get_incoming_channels(const tal_t *ctx UNNEEDED, const bool *private_too UNNEEDED) { fprintf(stderr, "towire_gossip_get_incoming_channels called!\n"); abort(); } /* Generated stub for towire_hsm_get_channel_basepoints */ u8 *towire_hsm_get_channel_basepoints(const tal_t *ctx UNNEEDED, const struct pubkey *peerid UNNEEDED, u64 dbid UNNEEDED) diff --git a/tests/test_invoices.py b/tests/test_invoices.py index 8f05bf56d..d7776953f 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -194,6 +194,11 @@ def test_invoice_routeboost_private(node_factory, bitcoind): assert r['fee_proportional_millionths'] == 10 assert r['cltv_expiry_delta'] == 6 + # If we explicitly say not to, it won't expose. + inv = l2.rpc.invoice(msatoshi=123456, label="inv1", description="?", exposeprivatechannels=False) + assert 'warning_capacity' in inv + assert 'routes' not in l1.rpc.decodepay(inv['bolt11']) + # The existence of a public channel, even without capacity, will suppress # the exposure of private channels. l3 = node_factory.get_node() @@ -204,9 +209,21 @@ def test_invoice_routeboost_private(node_factory, bitcoind): # Make sure channel is totally public. wait_for(lambda: [c['public'] for c in l3.rpc.listchannels(scid)['channels']] == [True, True]) - inv = l2.rpc.invoice(msatoshi=10**7, label="inv1", description="?") + inv = l2.rpc.invoice(msatoshi=10**7, label="inv2", description="?") assert 'warning_capacity' in inv + # Unless we tell it to include it. + inv = l2.rpc.invoice(msatoshi=10**7, label="inv3", description="?", exposeprivatechannels=True) + assert 'warning_capacity' not in inv + assert 'warning_offline' not in inv + # Route array has single route with single element. + r = only_one(only_one(l1.rpc.decodepay(inv['bolt11'])['routes'])) + assert r['pubkey'] == l1.info['id'] + assert r['short_channel_id'] == l1.rpc.listchannels()['channels'][0]['short_channel_id'] + assert r['fee_base_msat'] == 1 + assert r['fee_proportional_millionths'] == 10 + assert r['cltv_expiry_delta'] == 6 + def test_invoice_expiry(node_factory, executor): l1, l2 = node_factory.line_graph(2, fundchannel=True) diff --git a/tools/generate-wire.py b/tools/generate-wire.py index ceb3c4098..46a519e4d 100755 --- a/tools/generate-wire.py +++ b/tools/generate-wire.py @@ -392,29 +392,34 @@ class Message(object): self.print_fromwire_array('*' + f.name, subcalls, basetype, f, '*' + f.name, f.lenvar) else: + if f.optional: + assignable = f.fieldtype.is_assignable() + deref = '*' + else: + deref = '' + assignable = f.is_assignable() + + if assignable: + if f.is_len_var: + s = '{} = fromwire_{}(&cursor, &plen);'.format(f.name, basetype) + else: + s = '{}*{} = fromwire_{}(&cursor, &plen);'.format(deref, f.name, basetype) + elif basetype in varlen_structs: + s = '{}*{} = fromwire_{}(ctx, &cursor, &plen);'.format(deref, f.name, basetype) + else: + s = 'fromwire_{}(&cursor, &plen, {}{});'.format(basetype, deref, f.name) + if f.optional: subcalls.append("if (!fromwire_bool(&cursor, &plen))\n" "*{} = NULL;\n" "else {{\n" "*{} = tal(ctx, {});\n" - "fromwire_{}(&cursor, &plen, *{});\n" + "{}\n" "}}" .format(f.name, f.name, f.fieldtype.name, - basetype, f.name)) - elif f.is_assignable(): - subcalls.append("//3rd case {name}".format(name=f.name)) - if f.is_len_var: - subcalls.append('{} = fromwire_{}(&cursor, &plen);' - .format(f.name, basetype)) - else: - subcalls.append('*{} = fromwire_{}(&cursor, &plen);' - .format(f.name, basetype)) - elif basetype in varlen_structs: - subcalls.append('*{} = fromwire_{}(ctx, &cursor, &plen);' - .format(f.name, basetype)) + s)) else: - subcalls.append('fromwire_{}(&cursor, &plen, {});' - .format(basetype, f.name)) + subcalls.append(s) return template.format( name=self.name, @@ -481,12 +486,16 @@ class Message(object): self.print_towire_array(subcalls, basetype, f, f.lenvar) else: if f.optional: + if f.fieldtype.is_assignable(): + deref = '*' + else: + deref = '' subcalls.append("if (!{})\n" "towire_bool(&p, false);\n" "else {{\n" "towire_bool(&p, true);\n" - "towire_{}(&p, {});\n" - "}}".format(f.name, basetype, f.name)) + "towire_{}(&p, {}{});\n" + "}}".format(f.name, basetype, deref, f.name)) else: subcalls.append('towire_{}(&p, {});' .format(basetype, f.name))