JSON RPC: invoice exposeprivatechannels can specify exact channels.

Changelog-Changed: JSON API: `invoice` `exposeprivatechannels` can specify exact channel candidates.
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
Rusty Russell 2019-12-17 16:48:13 +10:30 committed by ZmnSCPxj, ZmnSCPxj jxPCSmnZ
parent 11dc1b341c
commit c74fceb4c9
5 changed files with 160 additions and 30 deletions

View File

@ -45,5 +45,6 @@
/* Errors from `invoice` command */
#define INVOICE_LABEL_ALREADY_EXISTS 900
#define INVOICE_PREIMAGE_ALREADY_EXISTS 901
#define INVOICE_HINTS_GAVE_NO_ROUTES 902
#endif /* LIGHTNING_COMMON_JSONRPC_ERRORS_H */

View File

@ -59,7 +59,9 @@ should not be used unless explicitly needed\.
If specified, \fIexposeprivatechannels\fR overrides the default route hint
logic, which will use unpublished channels only if there are no
published channels\. If \fItrue\fR unpublished channels are always considered
as a route hint candidate; if \fIfalse\fR, never\.
as a route hint candidate; if \fIfalse\fR, never\. If it is a short channel id
(e\.g\. \fI1x1x3\fR) or array of short channel ids, only those specific channels
will be considered candidates, even if they are public\.
The route hint is selected from the set of incoming channels of which:
@ -91,6 +93,8 @@ The following error codes may occur:
900: An invoice with the given \fIlabel\fR already exists\.
.IP \[bu]
901: An invoice with the given \fIpreimage\fR already exists\.
.IP \[bu]
902: None of the specified \fIexposeprivatechannels\fR were usable\.
.RE
@ -101,9 +105,10 @@ One of the following warnings may occur (on success):
\fIwarning_offline\fR if no channel with a currently connected peer has
the incoming capacity to pay this invoice
.IP \[bu]
\fIwarning_capacity\fR if there is no channel that has both sufficient
incoming capacity and has a peer that is publicly connected (i\.e\.
not a dead end)
\fIwarning_capacity\fR if there is no channel that has sufficient
incoming capacity
.IP \[bu]
\fIwarning_deadends\fR if there is no channel that is not a dead-end
.RE
.SH AUTHOR

View File

@ -54,7 +54,9 @@ 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.
as a route hint candidate; if *false*, never. If it is a short channel id
(e.g. *1x1x3*) or array of short channel ids, only those specific channels
will be considered candidates, even if they are public.
The route hint is selected from the set of incoming channels of which:
peers balance minus their reserves is at least *msatoshi*, state is
@ -79,13 +81,14 @@ The following error codes may occur:
- -1: Catchall nonspecific error.
- 900: An invoice with the given *label* already exists.
- 901: An invoice with the given *preimage* already exists.
- 902: None of the specified *exposeprivatechannels* were usable.
One of the following warnings may occur (on success):
- *warning\_offline* if no channel with a currently connected peer has
the incoming capacity to pay this invoice
- *warning\_capacity* if there is no channel that has both sufficient
incoming capacity and has a peer that is publicly connected (i.e.
not a dead end)
- *warning\_capacity* if there is no channel that has sufficient
incoming capacity
- *warning\_deadends* if there is no channel that is not a dead-end
AUTHOR
------

View File

@ -537,6 +537,16 @@ static bool all_true(const bool *barr, size_t n)
return true;
}
static bool scid_in_arr(const struct short_channel_id *scidarr,
const struct short_channel_id *scid)
{
for (size_t i = 0; i < tal_count(scidarr); i++)
if (short_channel_id_eq(&scidarr[i], scid))
return true;
return false;
}
static void gossipd_incoming_channels_reply(struct subd *gossipd,
const u8 *msg,
const int *fs,
@ -566,13 +576,33 @@ static void gossipd_incoming_channels_reply(struct subd *gossipd,
inchan_deadends = tal_arr(tmpctx, bool, 0);
}
if (chanhints->expose_all_private) {
if (chanhints && chanhints->expose_all_private) {
append_routes(&inchans, private);
append_bools(&inchan_deadends, private_deadends);
} else if (chanhints->hints) {
/* FIXME: Implement hint support! */
assert(!tal_count(chanhints->hints));
} else if (chanhints && chanhints->hints) {
/* Start by considering all channels as candidates */
append_routes(&inchans, private);
append_bools(&inchan_deadends, private_deadends);
/* Consider only hints they gave */
for (size_t i = 0; i < tal_count(inchans); i++) {
if (!scid_in_arr(chanhints->hints,
&inchans[i].short_channel_id)) {
tal_arr_remove(&inchans, i);
tal_arr_remove(&inchan_deadends, i);
}
}
/* If they told us to use scids and we couldn't, fail. */
if (tal_count(inchans) == 0
&& tal_count(chanhints->hints) != 0) {
was_pending(command_fail(info->cmd,
INVOICE_HINTS_GAVE_NO_ROUTES,
"None of those hints were suitable local channels"));
return;
}
} else {
assert(!chanhints);
/* By default, only consider private channels if there are
* no public channels *at all* */
if (tal_count(inchans) == 0) {
@ -787,6 +817,50 @@ static struct command_result *param_time(struct command *cmd, const char *name,
name, tok->end - tok->start, buffer + tok->start);
}
static struct command_result *param_chanhints(struct command *cmd,
const char *name,
const char *buffer,
const jsmntok_t *tok,
struct chanhints **chanhints)
{
bool boolhint;
*chanhints = tal(cmd, struct chanhints);
/* Could be simply "true" or "false" */
if (json_to_bool(buffer, tok, &boolhint)) {
(*chanhints)->expose_all_private = boolhint;
(*chanhints)->hints
= tal_arr(*chanhints, struct short_channel_id, 0);
return NULL;
}
(*chanhints)->expose_all_private = false;
/* Could be a single short_channel_id or an array */
if (tok->type == JSMN_ARRAY) {
size_t i;
const jsmntok_t *t;
(*chanhints)->hints
= tal_arr(*chanhints, struct short_channel_id,
tok->size);
json_for_each_arr(i, t, tok) {
if (!json_to_short_channel_id(buffer, t,
&(*chanhints)->hints[i])) {
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"'%s' should be a short channel id, not '%.*s'",
name, json_tok_full_len(t),
json_tok_full(buffer, t));
}
}
return NULL;
}
/* Otherwise should be a short_channel_id */
return param_short_channel_id(cmd, name, buffer, tok,
&(*chanhints)->hints);
}
static struct command_result *json_invoice(struct command *cmd,
const char *buffer,
const jsmntok_t *obj UNNEEDED,
@ -800,7 +874,6 @@ static struct command_result *json_invoice(struct command *cmd,
const u8 **fallback_scripts = NULL;
u64 *expiry;
struct sha256 rhash;
bool *exposeprivate;
struct secret payment_secret;
#if DEVELOPER
const jsmntok_t *routes;
@ -808,7 +881,6 @@ static struct command_result *json_invoice(struct command *cmd,
info = tal(cmd, struct invoice_info);
info->cmd = cmd;
info->chanhints = tal(info, struct chanhints);
if (!param(cmd, buffer, params,
p_req("msatoshi", param_msat_or_any, &msatoshi_val),
@ -817,7 +889,8 @@ static struct command_result *json_invoice(struct command *cmd,
p_opt_def("expiry", param_time, &expiry, 3600*24*7),
p_opt("fallbacks", param_array, &fallbacks),
p_opt("preimage", param_tok, &preimagetok),
p_opt("exposeprivatechannels", param_bool, &exposeprivate),
p_opt("exposeprivatechannels", param_chanhints,
&info->chanhints),
#if DEVELOPER
p_opt("dev-routes", param_array, &routes),
#endif
@ -839,17 +912,6 @@ static struct command_result *json_invoice(struct command *cmd,
strlen(desc_val));
}
/* Default is expose iff no public channels. */
if (exposeprivate == NULL) {
info->chanhints->expose_all_private = false;
info->chanhints->hints = NULL;
} else {
info->chanhints->expose_all_private = *exposeprivate;
/* FIXME: Support hints! */
info->chanhints->hints = tal_arr(info->chanhints,
struct short_channel_id, 0);
}
if (msatoshi_val
&& amount_msat_greater(*msatoshi_val, chainparams->max_payment)) {
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,

View File

@ -185,14 +185,16 @@ def test_invoice_routeboost_private(node_factory, bitcoind):
"""
l1, l2 = node_factory.line_graph(2, fundamount=16777215, announce_channels=False)
scid = l1.get_channel_scid(l2)
# Attach public channel to l1 so it doesn't look like a dead-end.
l0 = node_factory.get_node()
l0.rpc.connect(l1.info['id'], 'localhost', l1.port)
scid = l0.fund_channel(l1, 2 * (10**5))
scid_dummy = l0.fund_channel(l1, 2 * (10**5))
bitcoind.generate_block(5)
# Make sure channel is totally public.
wait_for(lambda: [c['public'] for c in l2.rpc.listchannels(scid)['channels']] == [True, True])
wait_for(lambda: [c['public'] for c in l2.rpc.listchannels(scid_dummy)['channels']] == [True, True])
# Since there's only one route, it will reluctantly hint that even
# though it's private
@ -215,15 +217,41 @@ def test_invoice_routeboost_private(node_factory, bitcoind):
assert 'warning_deadends' not in inv
assert 'routes' not in l1.rpc.decodepay(inv['bolt11'])
# If we ask for it, we get it.
inv = l2.rpc.invoice(msatoshi=123456, label="inv1a", description="?", exposeprivatechannels=scid)
assert 'warning_capacity' not in inv
assert 'warning_offline' not in inv
assert 'warning_deadends' 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
# Similarly if we ask for an array.
inv = l2.rpc.invoice(msatoshi=123456, label="inv1b", description="?", exposeprivatechannels=[scid])
assert 'warning_capacity' not in inv
assert 'warning_offline' not in inv
assert 'warning_deadends' 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
# The existence of a public channel, even without capacity, will suppress
# the exposure of private channels.
l3 = node_factory.get_node()
l3.rpc.connect(l2.info['id'], 'localhost', l2.port)
scid = l3.fund_channel(l2, (10**5))
scid2 = l3.fund_channel(l2, (10**5))
bitcoind.generate_block(5)
# Make sure channel is totally public.
wait_for(lambda: [c['public'] for c in l3.rpc.listchannels(scid)['channels']] == [True, True])
wait_for(lambda: [c['public'] for c in l2.rpc.listchannels(scid2)['channels']] == [True, True])
inv = l2.rpc.invoice(msatoshi=10**7, label="inv2", description="?")
assert 'warning_deadends' in inv
@ -243,6 +271,37 @@ def test_invoice_routeboost_private(node_factory, bitcoind):
assert r['fee_proportional_millionths'] == 10
assert r['cltv_expiry_delta'] == 6
inv = l2.rpc.invoice(msatoshi=10**7, label="inv4", description="?", exposeprivatechannels=scid)
assert 'warning_capacity' not in inv
assert 'warning_offline' not in inv
assert 'warning_deadends' 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'] == scid
assert r['fee_base_msat'] == 1
assert r['fee_proportional_millionths'] == 10
assert r['cltv_expiry_delta'] == 6
# Ask it explicitly to use a channel it can't (insufficient capacity)
inv = l2.rpc.invoice(msatoshi=10, label="inv5", description="?", exposeprivatechannels=scid2)
assert 'warning_deadends' in inv
assert 'warning_capacity' not in inv
assert 'warning_offline' not in inv
# Give it two options and it will pick one with suff capacity.
inv = l2.rpc.invoice(msatoshi=10, label="inv6", description="?", exposeprivatechannels=[scid2, scid])
assert 'warning_capacity' not in inv
assert 'warning_offline' not in inv
assert 'warning_deadends' 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'] == scid
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)