Skip to content

Commit

Permalink
JSON RPC: invoice exposeprivatechannels can specify exact channels.
Browse files Browse the repository at this point in the history
Changelog-Changed: JSON API: `invoice` `exposeprivatechannels` can specify exact channel candidates.
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
  • Loading branch information
rustyrussell committed Dec 17, 2019
1 parent e7061ff commit a436922
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 30 deletions.
1 change: 1 addition & 0 deletions common/jsonrpc_errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 9 additions & 4 deletions doc/lightning-invoice.7

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions doc/lightning-invoice.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
------
Expand Down
98 changes: 80 additions & 18 deletions lightningd/invoice.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand All @@ -800,15 +874,13 @@ 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;
#endif

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),
Expand All @@ -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
Expand All @@ -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,
Expand Down
67 changes: 63 additions & 4 deletions tests/test_invoices.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down

0 comments on commit a436922

Please sign in to comment.