mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-01-18 13:25:43 +01:00
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:
parent
11dc1b341c
commit
c74fceb4c9
@ -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 */
|
||||
|
13
doc/lightning-invoice.7
generated
13
doc/lightning-invoice.7
generated
@ -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
|
||||
|
@ -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:
|
||||
peer’s 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
|
||||
------
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user