We didn't remove them from the graph if they were disconnected, leading to us trying to send an onion message to it:
```
➜ ~ lightning-cli fetchinvoice lno1pg7y7s69g98zq5rp09hh2arnypnx7u3qvf3nzut68pmkcet2xdm8s7ngw5ux2am4wqekwmtkddkkk7nyv45ryefexum82anr8phpp6qrse80qf0aara4slvcjxrvu6j2rp5ftmjy4yntlsmsutpkvkt6878sy4xtpdc0j42xp72an4cl0unvzm7ckx2e6ltlzlgeekh0hhe0mgs2qgpetp66ufc9pwln9gmyv0gk3ndpqvvtynef8adzm3lxv0astkxunjcqx0fsq8kmx5cz574ft45vcweaf3tffp90fjdfl9gkdh4s6xfxaz7srtmsju9gnr3xerjhjqw4xtsmp55f4jjsxxrch703kx020l6wn2ttcd24w5h8f2lz723lk2kpa8ftkernz7h2qqkd3zecz2jmhucwh555xf0np4w45zm8tlka4ktw6d3ne4l9u678y37d24xjcy3el53faulcu5tzzquxrxvcyrvzgk7tyjeapt8wu858m2msgdznhqxl8fps04lef9dc9c 1sat
{
"code": -1,
"message": "onion msg: unknown next peer 024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605"
}
```
Fixes: https://github.com/ElementsProject/lightning/issues/8225
Changelog-Fixed: JSON-RPC: `fetchinvoice` is now more reliable.
282 lines
7.8 KiB
C
282 lines
7.8 KiB
C
#include "config.h"
|
|
#include <ccan/tal/str/str.h>
|
|
#include <common/dijkstra.h>
|
|
#include <common/gossmap.h>
|
|
#include <common/gossmods_listpeerchannels.h>
|
|
#include <common/json_stream.h>
|
|
#include <common/route.h>
|
|
#include <plugins/establish_onion_path.h>
|
|
|
|
struct connect_info {
|
|
struct pubkey local_id, dst;
|
|
bool connect_disable;
|
|
struct gossmap *gossmap;
|
|
struct command_result *(*cb)(struct command *,
|
|
const struct pubkey *,
|
|
void *arg);
|
|
struct command_result *(*fail)(struct command *, const char *,
|
|
void *arg);
|
|
void *arg;
|
|
};
|
|
|
|
static struct command_result *connect_ok(struct command *cmd,
|
|
const char *method,
|
|
const char *buf,
|
|
const jsmntok_t *result,
|
|
struct connect_info *ci)
|
|
{
|
|
struct pubkey *path = tal_arr(tmpctx, struct pubkey, 2);
|
|
|
|
/* Create direct mini-path */
|
|
path[0] = ci->local_id;
|
|
path[1] = ci->dst;
|
|
|
|
return ci->cb(cmd, path, ci->arg);
|
|
}
|
|
|
|
static struct command_result *command_failed(struct command *cmd,
|
|
const char *method,
|
|
const char *buf,
|
|
const jsmntok_t *result,
|
|
struct connect_info *ci)
|
|
{
|
|
return ci->fail(cmd, json_strdup(tmpctx, buf, result), ci->arg);
|
|
}
|
|
|
|
static struct command_result *connect_direct(struct command *cmd,
|
|
struct connect_info *ci)
|
|
{
|
|
struct out_req *req;
|
|
|
|
if (ci->connect_disable) {
|
|
return ci->fail(cmd, "fetchinvoice-noconnect set: not initiating a new connection",
|
|
ci->arg);
|
|
}
|
|
plugin_log(cmd->plugin, LOG_DBG, "connecting directly to %s",
|
|
fmt_pubkey(tmpctx, &ci->dst));
|
|
|
|
req = jsonrpc_request_start(cmd,
|
|
"connect", connect_ok, command_failed, ci);
|
|
json_add_pubkey(req->js, "id", &ci->dst);
|
|
return send_outreq(req);
|
|
}
|
|
|
|
static bool can_carry_onionmsg(const struct gossmap *map,
|
|
const struct gossmap_chan *c,
|
|
int dir,
|
|
struct amount_msat amount UNUSED,
|
|
void *arg UNUSED)
|
|
{
|
|
const struct gossmap_node *n;
|
|
|
|
/* Don't try to use disabled channels! */
|
|
if (!c->half[dir].enabled)
|
|
return false;
|
|
|
|
/* Our local additions are always fine, since we checked features then */
|
|
if (gossmap_chan_is_localmod(map, c))
|
|
return true;
|
|
|
|
/* Check features of recipient */
|
|
n = gossmap_nth_node(map, c, !dir);
|
|
return gossmap_node_get_feature(map, n, OPT_ONION_MESSAGES) != -1;
|
|
}
|
|
|
|
static void local_mods_remove_channels(struct gossmap_localmods *mods,
|
|
const struct gossmap *gossmap,
|
|
const struct node_id *selfid,
|
|
const struct node_id *peerid)
|
|
{
|
|
const struct gossmap_node *peer = gossmap_find_node(gossmap, peerid);
|
|
const struct gossmap_node *self = gossmap_find_node(gossmap, selfid);
|
|
const bool enabled_off = false;
|
|
|
|
if (!peer || !self)
|
|
return;
|
|
|
|
for (size_t i = 0; i < self->num_chans; i++) {
|
|
int dir;
|
|
struct short_channel_id_dir scidd;
|
|
struct gossmap_chan *c = gossmap_nth_chan(gossmap, self, i, &dir);
|
|
|
|
if (gossmap_nth_node(gossmap, c, !dir) != peer)
|
|
continue;
|
|
scidd.scid = gossmap_chan_scid(gossmap, c);
|
|
scidd.dir = dir;
|
|
|
|
/* Set enabled -> false for this channel */
|
|
gossmap_local_updatechan(mods, &scidd, &enabled_off,
|
|
NULL, NULL, NULL, NULL, NULL);
|
|
}
|
|
}
|
|
|
|
/* We add fake channels to gossmap to represent current outgoing connections.
|
|
* This allows dijkstra to find transient connections as well. */
|
|
static struct gossmap_localmods *
|
|
gossmods_from_listpeers(const tal_t *ctx,
|
|
struct plugin *plugin,
|
|
const struct gossmap *gossmap,
|
|
const struct node_id *self,
|
|
const char *buf,
|
|
const jsmntok_t *toks)
|
|
{
|
|
struct gossmap_localmods *mods = gossmap_localmods_new(ctx);
|
|
const jsmntok_t *peers, *peer;
|
|
size_t i;
|
|
|
|
peers = json_get_member(buf, toks, "peers");
|
|
json_for_each_arr(i, peer, peers) {
|
|
bool connected;
|
|
struct node_id peer_id;
|
|
const char *err;
|
|
u8 *features = NULL;
|
|
struct short_channel_id_dir fake_scidd;
|
|
bool enabled = true;
|
|
|
|
err = json_scan(tmpctx, buf, peer,
|
|
"{connected:%,"
|
|
"id:%,"
|
|
"features?:%}",
|
|
JSON_SCAN(json_to_bool, &connected),
|
|
JSON_SCAN(json_to_node_id, &peer_id),
|
|
JSON_SCAN_TAL(tmpctx, json_tok_bin_from_hex, &features));
|
|
if (err) {
|
|
plugin_err(plugin, "Bad listpeers.peers %zu: %s", i, err);
|
|
}
|
|
|
|
if (!feature_offered(features, OPT_ONION_MESSAGES))
|
|
continue;
|
|
|
|
if (!connected) {
|
|
/* Discard any channels we have with that. */
|
|
local_mods_remove_channels(mods, gossmap, self, &peer_id);
|
|
continue;
|
|
}
|
|
|
|
/* Add a fake channel */
|
|
fake_scidd.scid.u64 = i;
|
|
fake_scidd.dir = node_id_idx(self, &peer_id);
|
|
|
|
gossmap_local_addchan(mods, self, &peer_id, fake_scidd.scid,
|
|
AMOUNT_MSAT(1000), NULL);
|
|
gossmap_local_updatechan(mods, &fake_scidd, &enabled,
|
|
NULL, NULL, NULL, NULL, NULL);
|
|
}
|
|
return mods;
|
|
}
|
|
|
|
static const struct pubkey *path_to_node(const tal_t *ctx,
|
|
struct command *cmd,
|
|
struct gossmap *gossmap,
|
|
struct plugin *plugin,
|
|
const char *buf,
|
|
const jsmntok_t *listpeers,
|
|
const struct pubkey *local_id,
|
|
const struct pubkey *dst_key)
|
|
{
|
|
struct route_hop *r;
|
|
const struct dijkstra *dij;
|
|
const struct gossmap_node *src;
|
|
const struct gossmap_node *dst;
|
|
struct pubkey *nodes;
|
|
struct gossmap_localmods *mods;
|
|
struct node_id local_nodeid, dst_nodeid;
|
|
|
|
node_id_from_pubkey(&local_nodeid, local_id);
|
|
node_id_from_pubkey(&dst_nodeid, dst_key);
|
|
mods = gossmods_from_listpeers(tmpctx, cmd->plugin, gossmap, &local_nodeid, buf, listpeers);
|
|
|
|
gossmap_apply_localmods(gossmap, mods);
|
|
dst = gossmap_find_node(gossmap, &dst_nodeid);
|
|
if (!dst)
|
|
goto fail;
|
|
|
|
/* If we don't exist in gossip, routing can't happen. */
|
|
src = gossmap_find_node(gossmap, &local_nodeid);
|
|
if (!src)
|
|
goto fail;
|
|
|
|
dij = dijkstra(tmpctx, gossmap, dst, AMOUNT_MSAT(0), 0,
|
|
can_carry_onionmsg, route_score_shorter, NULL);
|
|
|
|
r = route_from_dijkstra(tmpctx, gossmap, dij, src, AMOUNT_MSAT(0), 0);
|
|
if (!r)
|
|
goto fail;
|
|
|
|
nodes = tal_arr(ctx, struct pubkey, tal_count(r) + 1);
|
|
nodes[0] = *local_id;
|
|
plugin_log(plugin, LOG_DBG, "Found path to %s: %s(us)",
|
|
fmt_node_id(tmpctx, &dst_nodeid),
|
|
fmt_pubkey(tmpctx, local_id));
|
|
for (size_t i = 0; i < tal_count(r); i++) {
|
|
if (!pubkey_from_node_id(&nodes[i+1], &r[i].node_id)) {
|
|
plugin_err(plugin, "Could not convert nodeid %s",
|
|
fmt_node_id(tmpctx, &r[i].node_id));
|
|
}
|
|
plugin_log(plugin, LOG_DBG, "-> %s",
|
|
fmt_node_id(tmpctx, &r[i].node_id));
|
|
}
|
|
|
|
gossmap_remove_localmods(gossmap, mods);
|
|
return nodes;
|
|
|
|
fail:
|
|
gossmap_remove_localmods(gossmap, mods);
|
|
return NULL;
|
|
}
|
|
|
|
static struct command_result *listpeers_done(struct command *cmd,
|
|
const char *method,
|
|
const char *buf,
|
|
const jsmntok_t *result,
|
|
struct connect_info *ci)
|
|
{
|
|
const struct pubkey *path;
|
|
|
|
path = path_to_node(tmpctx, cmd,
|
|
ci->gossmap, cmd->plugin, buf, result,
|
|
&ci->local_id, &ci->dst);
|
|
if (!path)
|
|
return connect_direct(cmd, ci);
|
|
|
|
return ci->cb(cmd, path, ci->arg);
|
|
}
|
|
|
|
struct command_result *establish_onion_path_(struct command *cmd,
|
|
struct gossmap *gossmap,
|
|
const struct pubkey *local_id,
|
|
const struct pubkey *dst,
|
|
bool connect_disable,
|
|
struct command_result *(*success)(struct command *,
|
|
const struct pubkey *,
|
|
void *arg),
|
|
struct command_result *(*fail)(struct command *,
|
|
const char *why,
|
|
void *arg),
|
|
void *arg)
|
|
{
|
|
struct connect_info *ci = tal(cmd, struct connect_info);
|
|
struct out_req *req;
|
|
|
|
/* Talking to ourselves is trivial. */
|
|
if (pubkey_eq(local_id, dst)) {
|
|
struct pubkey *path = tal_dup_arr(tmpctx, struct pubkey, local_id, 1, 0);
|
|
return success(cmd, path, arg);
|
|
}
|
|
|
|
ci->local_id = *local_id;
|
|
ci->dst = *dst;
|
|
ci->cb = success;
|
|
ci->fail = fail;
|
|
ci->arg = arg;
|
|
ci->connect_disable = connect_disable;
|
|
ci->gossmap = gossmap;
|
|
|
|
/* We use listpeers here: we don't actually care about channels, just connections! */
|
|
req = jsonrpc_request_start(cmd, "listpeers",
|
|
listpeers_done,
|
|
command_failed,
|
|
ci);
|
|
return send_outreq(req);
|
|
}
|