mirror of
https://github.com/aljazceru/lightning.git
synced 2025-12-20 07:34:24 +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:
committed by
ZmnSCPxj, ZmnSCPxj jxPCSmnZ
parent
11dc1b341c
commit
c74fceb4c9
@@ -45,5 +45,6 @@
|
|||||||
/* Errors from `invoice` command */
|
/* Errors from `invoice` command */
|
||||||
#define INVOICE_LABEL_ALREADY_EXISTS 900
|
#define INVOICE_LABEL_ALREADY_EXISTS 900
|
||||||
#define INVOICE_PREIMAGE_ALREADY_EXISTS 901
|
#define INVOICE_PREIMAGE_ALREADY_EXISTS 901
|
||||||
|
#define INVOICE_HINTS_GAVE_NO_ROUTES 902
|
||||||
|
|
||||||
#endif /* LIGHTNING_COMMON_JSONRPC_ERRORS_H */
|
#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
|
If specified, \fIexposeprivatechannels\fR overrides the default route hint
|
||||||
logic, which will use unpublished channels only if there are no
|
logic, which will use unpublished channels only if there are no
|
||||||
published channels\. If \fItrue\fR unpublished channels are always considered
|
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:
|
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\.
|
900: An invoice with the given \fIlabel\fR already exists\.
|
||||||
.IP \[bu]
|
.IP \[bu]
|
||||||
901: An invoice with the given \fIpreimage\fR already exists\.
|
901: An invoice with the given \fIpreimage\fR already exists\.
|
||||||
|
.IP \[bu]
|
||||||
|
902: None of the specified \fIexposeprivatechannels\fR were usable\.
|
||||||
|
|
||||||
.RE
|
.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
|
\fIwarning_offline\fR if no channel with a currently connected peer has
|
||||||
the incoming capacity to pay this invoice
|
the incoming capacity to pay this invoice
|
||||||
.IP \[bu]
|
.IP \[bu]
|
||||||
\fIwarning_capacity\fR if there is no channel that has both sufficient
|
\fIwarning_capacity\fR if there is no channel that has sufficient
|
||||||
incoming capacity and has a peer that is publicly connected (i\.e\.
|
incoming capacity
|
||||||
not a dead end)
|
.IP \[bu]
|
||||||
|
\fIwarning_deadends\fR if there is no channel that is not a dead-end
|
||||||
|
|
||||||
.RE
|
.RE
|
||||||
.SH AUTHOR
|
.SH AUTHOR
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ should not be used unless explicitly needed.
|
|||||||
If specified, *exposeprivatechannels* overrides the default route hint
|
If specified, *exposeprivatechannels* overrides the default route hint
|
||||||
logic, which will use unpublished channels only if there are no
|
logic, which will use unpublished channels only if there are no
|
||||||
published channels. If *true* unpublished channels are always considered
|
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:
|
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
|
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.
|
- -1: Catchall nonspecific error.
|
||||||
- 900: An invoice with the given *label* already exists.
|
- 900: An invoice with the given *label* already exists.
|
||||||
- 901: An invoice with the given *preimage* 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):
|
One of the following warnings may occur (on success):
|
||||||
- *warning\_offline* if no channel with a currently connected peer has
|
- *warning\_offline* if no channel with a currently connected peer has
|
||||||
the incoming capacity to pay this invoice
|
the incoming capacity to pay this invoice
|
||||||
- *warning\_capacity* if there is no channel that has both sufficient
|
- *warning\_capacity* if there is no channel that has sufficient
|
||||||
incoming capacity and has a peer that is publicly connected (i.e.
|
incoming capacity
|
||||||
not a dead end)
|
- *warning\_deadends* if there is no channel that is not a dead-end
|
||||||
|
|
||||||
AUTHOR
|
AUTHOR
|
||||||
------
|
------
|
||||||
|
|||||||
@@ -537,6 +537,16 @@ static bool all_true(const bool *barr, size_t n)
|
|||||||
return true;
|
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,
|
static void gossipd_incoming_channels_reply(struct subd *gossipd,
|
||||||
const u8 *msg,
|
const u8 *msg,
|
||||||
const int *fs,
|
const int *fs,
|
||||||
@@ -566,13 +576,33 @@ static void gossipd_incoming_channels_reply(struct subd *gossipd,
|
|||||||
inchan_deadends = tal_arr(tmpctx, bool, 0);
|
inchan_deadends = tal_arr(tmpctx, bool, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chanhints->expose_all_private) {
|
if (chanhints && chanhints->expose_all_private) {
|
||||||
append_routes(&inchans, private);
|
append_routes(&inchans, private);
|
||||||
append_bools(&inchan_deadends, private_deadends);
|
append_bools(&inchan_deadends, private_deadends);
|
||||||
} else if (chanhints->hints) {
|
} else if (chanhints && chanhints->hints) {
|
||||||
/* FIXME: Implement hint support! */
|
/* Start by considering all channels as candidates */
|
||||||
assert(!tal_count(chanhints->hints));
|
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 {
|
} else {
|
||||||
|
assert(!chanhints);
|
||||||
/* By default, only consider private channels if there are
|
/* By default, only consider private channels if there are
|
||||||
* no public channels *at all* */
|
* no public channels *at all* */
|
||||||
if (tal_count(inchans) == 0) {
|
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);
|
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,
|
static struct command_result *json_invoice(struct command *cmd,
|
||||||
const char *buffer,
|
const char *buffer,
|
||||||
const jsmntok_t *obj UNNEEDED,
|
const jsmntok_t *obj UNNEEDED,
|
||||||
@@ -800,7 +874,6 @@ static struct command_result *json_invoice(struct command *cmd,
|
|||||||
const u8 **fallback_scripts = NULL;
|
const u8 **fallback_scripts = NULL;
|
||||||
u64 *expiry;
|
u64 *expiry;
|
||||||
struct sha256 rhash;
|
struct sha256 rhash;
|
||||||
bool *exposeprivate;
|
|
||||||
struct secret payment_secret;
|
struct secret payment_secret;
|
||||||
#if DEVELOPER
|
#if DEVELOPER
|
||||||
const jsmntok_t *routes;
|
const jsmntok_t *routes;
|
||||||
@@ -808,7 +881,6 @@ static struct command_result *json_invoice(struct command *cmd,
|
|||||||
|
|
||||||
info = tal(cmd, struct invoice_info);
|
info = tal(cmd, struct invoice_info);
|
||||||
info->cmd = cmd;
|
info->cmd = cmd;
|
||||||
info->chanhints = tal(info, struct chanhints);
|
|
||||||
|
|
||||||
if (!param(cmd, buffer, params,
|
if (!param(cmd, buffer, params,
|
||||||
p_req("msatoshi", param_msat_or_any, &msatoshi_val),
|
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_def("expiry", param_time, &expiry, 3600*24*7),
|
||||||
p_opt("fallbacks", param_array, &fallbacks),
|
p_opt("fallbacks", param_array, &fallbacks),
|
||||||
p_opt("preimage", param_tok, &preimagetok),
|
p_opt("preimage", param_tok, &preimagetok),
|
||||||
p_opt("exposeprivatechannels", param_bool, &exposeprivate),
|
p_opt("exposeprivatechannels", param_chanhints,
|
||||||
|
&info->chanhints),
|
||||||
#if DEVELOPER
|
#if DEVELOPER
|
||||||
p_opt("dev-routes", param_array, &routes),
|
p_opt("dev-routes", param_array, &routes),
|
||||||
#endif
|
#endif
|
||||||
@@ -839,17 +912,6 @@ static struct command_result *json_invoice(struct command *cmd,
|
|||||||
strlen(desc_val));
|
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
|
if (msatoshi_val
|
||||||
&& amount_msat_greater(*msatoshi_val, chainparams->max_payment)) {
|
&& amount_msat_greater(*msatoshi_val, chainparams->max_payment)) {
|
||||||
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
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)
|
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.
|
# Attach public channel to l1 so it doesn't look like a dead-end.
|
||||||
l0 = node_factory.get_node()
|
l0 = node_factory.get_node()
|
||||||
l0.rpc.connect(l1.info['id'], 'localhost', l1.port)
|
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)
|
bitcoind.generate_block(5)
|
||||||
|
|
||||||
# Make sure channel is totally public.
|
# 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
|
# Since there's only one route, it will reluctantly hint that even
|
||||||
# though it's private
|
# though it's private
|
||||||
@@ -215,15 +217,41 @@ def test_invoice_routeboost_private(node_factory, bitcoind):
|
|||||||
assert 'warning_deadends' not in inv
|
assert 'warning_deadends' not in inv
|
||||||
assert 'routes' not in l1.rpc.decodepay(inv['bolt11'])
|
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 existence of a public channel, even without capacity, will suppress
|
||||||
# the exposure of private channels.
|
# the exposure of private channels.
|
||||||
l3 = node_factory.get_node()
|
l3 = node_factory.get_node()
|
||||||
l3.rpc.connect(l2.info['id'], 'localhost', l2.port)
|
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)
|
bitcoind.generate_block(5)
|
||||||
|
|
||||||
# Make sure channel is totally public.
|
# 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="?")
|
inv = l2.rpc.invoice(msatoshi=10**7, label="inv2", description="?")
|
||||||
assert 'warning_deadends' in inv
|
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['fee_proportional_millionths'] == 10
|
||||||
assert r['cltv_expiry_delta'] == 6
|
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):
|
def test_invoice_expiry(node_factory, executor):
|
||||||
l1, l2 = node_factory.line_graph(2, fundchannel=True)
|
l1, l2 = node_factory.line_graph(2, fundchannel=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user