diff --git a/plugins/channel_hint.c b/plugins/channel_hint.c index 3a6a57a28..c20410a04 100644 --- a/plugins/channel_hint.c +++ b/plugins/channel_hint.c @@ -71,6 +71,76 @@ bool channel_hint_update(const struct timeabs now, struct channel_hint *hint) amount_msat_greater(capacity, hint->estimated_capacity); } +struct channel_hint *channel_hint_set_find(struct channel_hint_set *self, + const struct short_channel_id_dir *scidd) +{ + for (size_t i=0; ihints); i++) { + struct channel_hint *hint = &self->hints[i]; + if (short_channel_id_dir_eq(&hint->scid, scidd)) + return hint; + } + return NULL; +} + +/* See header */ +struct channel_hint * +channel_hint_set_add(struct channel_hint_set *self, u32 timestamp, + const struct short_channel_id_dir *scidd, bool enabled, + const struct amount_msat *estimated_capacity, + const struct amount_sat capacity, u16 *htlc_budget) +{ + bool modified = false; + struct channel_hint *old, *newhint = tal(tmpctx, struct channel_hint); + struct timeabs now = time_now(); + + /* If the channel is marked as enabled it must have an estimate. */ + assert(!enabled || estimated_capacity != NULL); + newhint->enabled = enabled; + newhint->scid = *scidd; + newhint->capacity = capacity; + if (estimated_capacity != NULL) + newhint->estimated_capacity = *estimated_capacity; + newhint->local = NULL; + newhint->timestamp = timestamp; + + /* Project the channel_hints into the same domain, so we can merge them. + */ + channel_hint_update(now, newhint); + channel_hint_set_update(self, now); + + /* And now we can merge the new hint into the existing ones if there + are any. */ + old = channel_hint_set_find(self, scidd); + if (old == NULL) { + tal_arr_expand(&self->hints, *newhint); + // TODO extend the array + return &self->hints[tal_count(self->hints) - 1]; + } else { + /* Prefer to disable a channel. */ + if (!enabled && old->enabled) { + old->enabled = false; + modified = true; + } + /* Prefer the more conservative estimate. */ + if (estimated_capacity != NULL && + amount_msat_greater(old->estimated_capacity, + newhint->estimated_capacity)) { + old->estimated_capacity = newhint->estimated_capacity; + modified = true; + } + if (newhint->local) { + tal_free(old->local); + old->local = tal_steal(old, newhint->local); + } + } + if (modified) { + old->timestamp = timestamp; + return old; + } else { + return NULL; + } +} + /** * Load a channel_hint from its JSON representation. * diff --git a/plugins/channel_hint.h b/plugins/channel_hint.h index 5a2ffcbb1..1b09dd18c 100644 --- a/plugins/channel_hint.h +++ b/plugins/channel_hint.h @@ -7,7 +7,6 @@ #include #include #include -#include #include /* Information about channels we inferred from a) looking at our channels, and @@ -54,7 +53,7 @@ struct channel_hint_set { }; bool channel_hint_update(const struct timeabs now, - struct channel_hint *hint); + struct channel_hint *hint); void channel_hint_to_json(const char *name, const struct channel_hint *hint, struct json_stream *dest); @@ -68,4 +67,24 @@ struct channel_hint_set *channel_hint_set_new(const tal_t *ctx); /* Relax all channel_hints in this set, based on the time that has elapsed. */ void channel_hint_set_update(struct channel_hint_set *set, const struct timeabs now); +/** + * Look up a `channel_hint` from a `channel_hint_set` for a scidd. + */ +struct channel_hint *channel_hint_set_find(struct channel_hint_set *self, + const struct short_channel_id_dir *scidd); + +/** + * Add a new observation to the `channel_hint_set` + * + * This either adds a new entry, or updates an existing one in the set. + * @return A new channel_hint, if the addition resulted in changes. + */ +struct channel_hint *channel_hint_set_add(struct channel_hint_set *self, + u32 timestamp, + const struct short_channel_id_dir *scidd, + bool enabled, + const struct amount_msat *estimated_capacity, + const struct amount_sat overall_capacity, + u16 *htlc_budget); + #endif /* LIGHTNING_PLUGINS_CHANNEL_HINT_H */ diff --git a/plugins/keysend.c b/plugins/keysend.c index 99d46c1bb..75821bf0c 100644 --- a/plugins/keysend.c +++ b/plugins/keysend.c @@ -17,6 +17,7 @@ static unsigned int maxdelay_default; static struct node_id my_id; static u64 *accepted_extra_tlvs; +static struct channel_hint_set *global_hints; /***************************************************************************** * Keysend modifier @@ -159,6 +160,8 @@ static const char *init(struct plugin *p, const char *buf UNUSED, rpc_scan(p, "getinfo", take(json_out_obj(NULL, NULL, NULL)), "{id:%}", JSON_SCAN(json_to_node_id, &my_id)); + global_hints = notleak_with_children(channel_hint_set_new(p)); + accepted_extra_tlvs = notleak(tal_arr(NULL, u64, 0)); /* BOLT #4: * ## `max_htlc_cltv` Selection @@ -241,7 +244,7 @@ static struct command_result *json_keysend(struct command *cmd, const char *buf, NULL)) return command_param_failed(); - p = payment_new(cmd, cmd, NULL /* No parent */, pay_mods); + p = payment_new(cmd, cmd, NULL /* No parent */, global_hints, pay_mods); p->local_id = &my_id; p->json_buffer = tal_dup_talarr(p, const char, buf); p->json_toks = params; diff --git a/plugins/libplugin-pay.c b/plugins/libplugin-pay.c index b7bc1811c..ebaea797c 100644 --- a/plugins/libplugin-pay.c +++ b/plugins/libplugin-pay.c @@ -71,6 +71,7 @@ int libplugin_pay_poll(struct pollfd *fds, nfds_t nfds, int timeout) struct payment *payment_new(tal_t *ctx, struct command *cmd, struct payment *parent, + struct channel_hint_set *channel_hints, struct payment_modifier **mods) { struct payment *p = tal(ctx, struct payment); @@ -133,7 +134,6 @@ struct payment *payment_new(tal_t *ctx, struct command *cmd, p->partid = 0; p->next_partid = 1; p->plugin = cmd->plugin; - p->channel_hints = tal_arr(p, struct channel_hint, 0); p->excluded_nodes = tal_arr(p, struct node_id, 0); p->id = next_id++; p->description = NULL; @@ -143,6 +143,8 @@ struct payment *payment_new(tal_t *ctx, struct command *cmd, p->groupid = 0; p->mods = NULL; p->chainlag = 0; + assert(channel_hints != NULL); + p->hints = channel_hints; } /* Initialize all modifier data so we can point to the fields when @@ -158,6 +160,10 @@ struct payment *payment_new(tal_t *ctx, struct command *cmd, tal_arr_expand(&p->modifier_data, NULL); } + paymod_log(p, LOG_DBG, + "Initialized a new (sub-)payment with %zu channel_hints", + tal_count(payment_root(p)->hints->hints)); + return p; } @@ -413,82 +419,39 @@ static void channel_hints_update(struct payment *p, u16 *htlc_budget) { struct payment *root = payment_root(p); - struct channel_hint newhint; - u32 timestamp = time_now().ts.tv_sec; - memcheck(&overall_capacity, sizeof(struct amount_msat)); + struct short_channel_id_dir *scidd = + tal(tmpctx, struct short_channel_id_dir); + struct channel_hint *hint; + scidd->scid = scid; + scidd->dir = direction; + + /* Local channels must have an HTLC budget */ + assert(!local || htlc_budget != NULL); + + channel_hint_set_add(root->hints, time_now().ts.tv_sec, scidd, enabled, + estimated_capacity, overall_capacity, htlc_budget); + + hint = channel_hint_set_find(root->hints, scidd); + + if (local) { + hint->local = tal_free(hint->local); + hint->local = tal(root->hints, struct local_hint); + hint->local->htlc_budget = *htlc_budget; + } /* If the channel is marked as enabled it must have an estimate. */ assert(!enabled || estimated_capacity != NULL); - /* Try and look for an existing hint: */ - for (size_t i=0; ichannel_hints); i++) { - struct channel_hint *hint = &root->channel_hints[i]; - if (short_channel_id_eq(hint->scid.scid, scid) && - hint->scid.dir == direction) { - bool modified = false; - /* Prefer to disable a channel. */ - if (!enabled && hint->enabled) { - hint->enabled = false; - modified = true; - } - - /* Prefer the more conservative estimate. */ - if (estimated_capacity != NULL && - amount_msat_greater(hint->estimated_capacity, - *estimated_capacity)) { - hint->estimated_capacity = *estimated_capacity; - modified = true; - } - if (htlc_budget != NULL) { - assert(hint->local); - hint->local->htlc_budget = *htlc_budget; - modified = true; - } - - if (modified) { - hint->timestamp = timestamp; - paymod_log(p, LOG_DBG, - "Updated a channel hint for %s: " - "enabled %s, " - "estimated capacity %s", - fmt_short_channel_id_dir(tmpctx, - &hint->scid), - hint->enabled ? "true" : "false", - fmt_amount_msat(tmpctx, - hint->estimated_capacity)); - channel_hint_notify(p->plugin, hint); - } - return; - } + if (hint != NULL) { + paymod_log(p, LOG_DBG, + "Updated a channel hint for %s: " + "enabled %s, " + "estimated capacity %s", + fmt_short_channel_id_dir(tmpctx, &hint->scid), + hint->enabled ? "true" : "false", + fmt_amount_msat(tmpctx, hint->estimated_capacity)); + channel_hint_notify(p->plugin, hint); } - - /* No hint found, create one. */ - newhint.enabled = enabled; - newhint.timestamp = timestamp; - newhint.scid.scid = scid; - newhint.scid.dir = direction; - newhint.capacity = overall_capacity; - if (local) { - newhint.local = tal(root->channel_hints, struct local_hint); - assert(htlc_budget); - newhint.local->htlc_budget = *htlc_budget; - } else - newhint.local = NULL; - if (estimated_capacity != NULL) - newhint.estimated_capacity = *estimated_capacity; - else if (!amount_sat_to_msat(&newhint.estimated_capacity, - overall_capacity)) - abort(); - - tal_arr_expand(&root->channel_hints, newhint); - - paymod_log( - p, LOG_DBG, - "Added a channel hint for %s: enabled %s, estimated capacity %s", - fmt_short_channel_id_dir(tmpctx, &newhint.scid), - newhint.enabled ? "true" : "false", - fmt_amount_msat(tmpctx, newhint.estimated_capacity)); - channel_hint_notify(p->plugin, &newhint); } static void payment_exclude_most_expensive(struct payment *p) @@ -563,15 +526,8 @@ static struct channel_hint *payment_chanhints_get(struct payment *p, struct route_hop *h) { struct payment *root = payment_root(p); - struct channel_hint *curhint; - for (size_t j = 0; j < tal_count(root->channel_hints); j++) { - curhint = &root->channel_hints[j]; - if (short_channel_id_eq(curhint->scid.scid, h->scid) && - curhint->scid.dir == h->direction) { - return curhint; - } - } - return NULL; + struct short_channel_id_dir scidd = {.scid = h->scid, .dir = h->direction}; + return channel_hint_set_find(root->hints, &scidd); } /* Given a route and a couple of channel hints, apply the route to the channel @@ -697,8 +653,8 @@ payment_get_excluded_channels(const tal_t *ctx, struct payment *p) struct channel_hint *hint; struct short_channel_id_dir *res = tal_arr(ctx, struct short_channel_id_dir, 0); - for (size_t i = 0; i < tal_count(root->channel_hints); i++) { - hint = &root->channel_hints[i]; + for (size_t i = 0; i < tal_count(root->hints->hints); i++) { + hint = &root->hints->hints[i]; if (!hint->enabled) tal_arr_expand(&res, hint->scid); @@ -772,7 +728,7 @@ static bool payment_route_check(const struct gossmap *gossmap, return false; scid = gossmap_chan_scid(gossmap, c); - hint = find_hint(payment_root(p)->channel_hints, scid, dir); + hint = find_hint(payment_root(p)->hints->hints, scid, dir); if (!hint) return true; @@ -2606,7 +2562,7 @@ static inline void retry_step_cb(struct retry_mod_data *rd, /* If the failure was not final, and we tried a route, try again. */ if (rdata->retries > 0) { payment_set_step(p, PAYMENT_STEP_RETRY); - subpayment = payment_new(p, NULL, p, p->modifiers); + subpayment = payment_new(p, NULL, p, NULL, p->modifiers); payment_start(subpayment); subpayment->why = tal_fmt(subpayment, "Still have %d attempts left", @@ -2694,7 +2650,7 @@ local_channel_hints_listpeerchannels(struct command *cmd, const char *buffer, * otherwise start out as excluded and remain so until * forever. */ - struct channel_hint *hints = payment_root(p)->channel_hints; + struct channel_hint *hints = payment_root(p)->hints->hints; for (size_t i = 0; i < tal_count(hints); i++) channel_hint_update(time_now(), &hints[i]); @@ -2837,7 +2793,7 @@ static bool routehint_excluded(struct payment *p, const struct node_id *nodes = payment_get_excluded_nodes(tmpctx, p); const struct short_channel_id_dir *chans = payment_get_excluded_channels(tmpctx, p); - const struct channel_hint *hints = payment_root(p)->channel_hints; + const struct channel_hint_set *hints = payment_root(p)->hints; /* Note that we ignore direction here: in theory, we could have * found that one direction of a channel is unavailable, but they @@ -2878,12 +2834,12 @@ static bool routehint_excluded(struct payment *p, * channel, which is greater than the destination. */ for (size_t j = 0; j < tal_count(hints); j++) { - if (!short_channel_id_eq(hints[j].scid.scid, r->short_channel_id)) + if (!short_channel_id_eq(hints->hints[j].scid.scid, r->short_channel_id)) continue; /* We exclude on equality because we set the estimate * to the smallest failed attempt. */ if (amount_msat_greater_eq(needed_capacity, - hints[j].estimated_capacity)) + hints->hints[j].estimated_capacity)) return true; } } @@ -3526,15 +3482,7 @@ static void direct_pay_override(struct payment *p) { /* If we have a channel we need to make sure that it still has * sufficient capacity. Look it up in the channel_hints. */ - for (size_t i=0; ichannel_hints); i++) { - struct short_channel_id_dir *cur = &root->channel_hints[i].scid; - if (short_channel_id_eq(cur->scid, d->chan->scid) && - cur->dir == d->chan->dir) { - hint = &root->channel_hints[i]; - break; - } - } - + hint = channel_hint_set_find(root->hints, d->chan); if (hint && hint->enabled && amount_msat_greater(hint->estimated_capacity, p->our_amount)) { /* Now build a route that consists only of this single hop */ @@ -3636,11 +3584,12 @@ REGISTER_PAYMENT_MODIFIER(directpay, struct direct_pay_data *, direct_pay_init, static u32 payment_max_htlcs(const struct payment *p) { + return 10000;/* const struct payment *root; struct channel_hint *h; u32 res = 0; - for (size_t i = 0; i < tal_count(p->channel_hints); i++) { - h = &p->channel_hints[i]; + for (size_t i = 0; i < tal_count(p->hints->hints); i++) { + h = &p->hints->hints[i]; if (h->local && h->enabled) res += h->local->htlc_budget; } @@ -3649,7 +3598,7 @@ static u32 payment_max_htlcs(const struct payment *p) root = root->parent; if (res > root->max_htlcs) res = root->max_htlcs; - return res; + return res;*/ } /** payment_lower_max_htlcs @@ -3774,8 +3723,8 @@ static void adaptive_splitter_cb(struct adaptive_split_mod_data *d, struct payme } p->step = PAYMENT_STEP_SPLIT; - a = payment_new(p, NULL, p, p->modifiers); - b = payment_new(p, NULL, p, p->modifiers); + a = payment_new(p, NULL, p, NULL, p->modifiers); + b = payment_new(p, NULL, p, NULL, p->modifiers); a->our_amount.millisatoshis = mid; /* Raw: split. */ b->our_amount.millisatoshis -= mid; /* Raw: split. */ diff --git a/plugins/libplugin-pay.h b/plugins/libplugin-pay.h index 90d4e6ab7..efa253623 100644 --- a/plugins/libplugin-pay.h +++ b/plugins/libplugin-pay.h @@ -239,7 +239,7 @@ struct payment { /* tal_arr of channel_hints we incrementally learn while performing * payment attempts. */ - struct channel_hint *channel_hints; + struct channel_hint_set *hints; struct node_id *excluded_nodes; /* Optional temporarily excluded channels/nodes (i.e. this routehint) */ @@ -446,6 +446,7 @@ REGISTER_PAYMENT_MODIFIER_HEADER(route_exclusions, struct route_exclusions_data) struct payment *payment_new(tal_t *ctx, struct command *cmd, struct payment *parent, + struct channel_hint_set *channel_hints, struct payment_modifier **mods); void payment_start(struct payment *p); diff --git a/plugins/pay.c b/plugins/pay.c index 456d8b2c4..cf65e67c6 100644 --- a/plugins/pay.c +++ b/plugins/pay.c @@ -24,7 +24,7 @@ static struct node_id my_id; static unsigned int maxdelay_default; static bool exp_offers; static bool disablempp = false; -static struct channel_hint *global_hints; +static struct channel_hint_set *global_hints; static LIST_HEAD(payments); @@ -586,17 +586,15 @@ static const char *init(struct plugin *p, /* FIXME: Typo in spec for CLTV in descripton! But it breaks our spelling check, so we omit it above */ maxdelay_default = 2016; - global_hints = - notleak_with_children(tal_arr(p, struct channel_hint, 0)); + global_hints = notleak_with_children(channel_hint_set_new(p)); - /* max-locktime-blocks deprecated in v24.05, but still grab it! */ - rpc_scan(p, "listconfigs", - take(json_out_obj(NULL, NULL, NULL)), - "{configs:" - "{max-locktime-blocks?:{value_int:%}," - "experimental-offers:{set:%}}}", - JSON_SCAN(json_to_number, &maxdelay_default), - JSON_SCAN(json_to_bool, &exp_offers)); + /* max-locktime-blocks deprecated in v24.05, but still grab it! */ + rpc_scan(p, "listconfigs", take(json_out_obj(NULL, NULL, NULL)), + "{configs:" + "{max-locktime-blocks?:{value_int:%}," + "experimental-offers:{set:%}}}", + JSON_SCAN(json_to_number, &maxdelay_default), + JSON_SCAN(json_to_bool, &exp_offers)); plugin_set_memleak_handler(p, memleak_mark_payments); return NULL; @@ -1261,7 +1259,7 @@ static struct command_result *json_pay(struct command *cmd, NULL)) return command_param_failed(); - p = payment_new(cmd, cmd, NULL /* No parent */, paymod_mods); + p = payment_new(cmd, cmd, NULL /* No parent */, global_hints, paymod_mods); p->invstring = tal_steal(p, b11str); p->description = tal_steal(p, description); /* Overridded by bolt12 if present */ diff --git a/plugins/test/Makefile b/plugins/test/Makefile index 2ecba481c..3b5712d5a 100644 --- a/plugins/test/Makefile +++ b/plugins/test/Makefile @@ -20,14 +20,16 @@ plugins/test/run-route-overlong: \ common/gossmap.o \ common/node_id.o \ common/route.o \ - gossipd/gossip_store_wiregen.o + gossipd/gossip_store_wiregen.o \ + plugins/channel_hint.o plugins/test/run-route-calc: \ common/fp16.o \ common/gossmap.o \ common/node_id.o \ common/route.o \ - gossipd/gossip_store_wiregen.o + gossipd/gossip_store_wiregen.o \ + plugins/channel_hint.o $(PLUGIN_TEST_PROGRAMS): $(BITCOIN_OBJS) $(WIRE_OBJS) $(PLUGIN_TEST_COMMON_OBJS) diff --git a/plugins/test/run-route-calc.c b/plugins/test/run-route-calc.c index 918a42fbf..cf14bf786 100644 --- a/plugins/test/run-route-calc.c +++ b/plugins/test/run-route-calc.c @@ -1,4 +1,5 @@ #include "config.h" +#include "plugins/channel_hint.h" #define TESTING #include "../../common/dijkstra.c" #include "../libplugin-pay.c" @@ -18,14 +19,6 @@ u8 **blinded_onion_hops(const tal_t *ctx UNNEEDED, struct amount_msat total_amount UNNEEDED, const struct blinded_path *path UNNEEDED) { fprintf(stderr, "blinded_onion_hops called!\n"); abort(); } -/* Generated stub for channel_hint_to_json */ -void channel_hint_to_json(const char *name UNNEEDED, const struct channel_hint *hint UNNEEDED, - struct json_stream *dest UNNEEDED) -{ fprintf(stderr, "channel_hint_to_json called!\n"); abort(); } -/* Generated stub for channel_hint_update */ -bool channel_hint_update(const struct timeabs now UNNEEDED, - struct channel_hint *hint UNNEEDED) -{ fprintf(stderr, "channel_hint_update called!\n"); abort(); } /* Generated stub for command_finished */ struct command_result *command_finished(struct command *cmd UNNEEDED, struct json_stream *response UNNEEDED) { fprintf(stderr, "command_finished called!\n"); abort(); } @@ -91,6 +84,16 @@ void json_add_amount_msat(struct json_stream *result UNNEEDED, struct amount_msat msat) { fprintf(stderr, "json_add_amount_msat called!\n"); abort(); } +/* Generated stub for json_add_amount_sat */ +void json_add_amount_sat(struct json_stream *result UNNEEDED, + const char *satfieldname UNNEEDED, + struct amount_sat sat) + +{ fprintf(stderr, "json_add_amount_sat called!\n"); abort(); } +/* Generated stub for json_add_bool */ +void json_add_bool(struct json_stream *result UNNEEDED, const char *fieldname UNNEEDED, + bool value UNNEEDED) +{ fprintf(stderr, "json_add_bool called!\n"); abort(); } /* Generated stub for json_add_hex_talarr */ void json_add_hex_talarr(struct json_stream *result UNNEEDED, const char *fieldname UNNEEDED, @@ -126,6 +129,11 @@ void json_add_short_channel_id(struct json_stream *response UNNEEDED, const char *fieldname UNNEEDED, struct short_channel_id id UNNEEDED) { fprintf(stderr, "json_add_short_channel_id called!\n"); abort(); } +/* Generated stub for json_add_short_channel_id_dir */ +void json_add_short_channel_id_dir(struct json_stream *response UNNEEDED, + const char *fieldname UNNEEDED, + struct short_channel_id_dir idd UNNEEDED) +{ fprintf(stderr, "json_add_short_channel_id_dir called!\n"); abort(); } /* Generated stub for json_add_string */ void json_add_string(struct json_stream *js UNNEEDED, const char *fieldname UNNEEDED, @@ -214,6 +222,10 @@ bool json_to_sat(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, bool json_to_short_channel_id(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, struct short_channel_id *scid UNNEEDED) { fprintf(stderr, "json_to_short_channel_id called!\n"); abort(); } +/* Generated stub for json_to_short_channel_id_dir */ +bool json_to_short_channel_id_dir(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + struct short_channel_id_dir *scidd UNNEEDED) +{ fprintf(stderr, "json_to_short_channel_id_dir called!\n"); abort(); } /* Generated stub for json_to_u16 */ bool json_to_u16(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, uint16_t *num UNNEEDED) @@ -418,12 +430,14 @@ int main(int argc, char *argv[]) { struct payment *p; struct payment_modifier **mods; + struct channel_hint_set *hints; common_setup(argv[0]); chainparams = chainparams_for_network("regtest"); + hints = channel_hint_set_new(tmpctx); mods = tal_arrz(tmpctx, struct payment_modifier *, 1); - p = payment_new(mods, tal(tmpctx, struct command), NULL, mods); + p = payment_new(mods, tal(tmpctx, struct command), NULL, hints, mods); /* We want to permute order of channels between each node, to * avoid "it works because it chooses the first one!" */ diff --git a/plugins/test/run-route-overlong.c b/plugins/test/run-route-overlong.c index 61635bf13..27a590418 100644 --- a/plugins/test/run-route-overlong.c +++ b/plugins/test/run-route-overlong.c @@ -16,14 +16,6 @@ u8 **blinded_onion_hops(const tal_t *ctx UNNEEDED, struct amount_msat total_amount UNNEEDED, const struct blinded_path *path UNNEEDED) { fprintf(stderr, "blinded_onion_hops called!\n"); abort(); } -/* Generated stub for channel_hint_to_json */ -void channel_hint_to_json(const char *name UNNEEDED, const struct channel_hint *hint UNNEEDED, - struct json_stream *dest UNNEEDED) -{ fprintf(stderr, "channel_hint_to_json called!\n"); abort(); } -/* Generated stub for channel_hint_update */ -bool channel_hint_update(const struct timeabs now UNNEEDED, - struct channel_hint *hint UNNEEDED) -{ fprintf(stderr, "channel_hint_update called!\n"); abort(); } /* Generated stub for command_finished */ struct command_result *command_finished(struct command *cmd UNNEEDED, struct json_stream *response UNNEEDED) { fprintf(stderr, "command_finished called!\n"); abort(); } @@ -89,6 +81,16 @@ void json_add_amount_msat(struct json_stream *result UNNEEDED, struct amount_msat msat) { fprintf(stderr, "json_add_amount_msat called!\n"); abort(); } +/* Generated stub for json_add_amount_sat */ +void json_add_amount_sat(struct json_stream *result UNNEEDED, + const char *satfieldname UNNEEDED, + struct amount_sat sat) + +{ fprintf(stderr, "json_add_amount_sat called!\n"); abort(); } +/* Generated stub for json_add_bool */ +void json_add_bool(struct json_stream *result UNNEEDED, const char *fieldname UNNEEDED, + bool value UNNEEDED) +{ fprintf(stderr, "json_add_bool called!\n"); abort(); } /* Generated stub for json_add_hex_talarr */ void json_add_hex_talarr(struct json_stream *result UNNEEDED, const char *fieldname UNNEEDED, @@ -124,6 +126,11 @@ void json_add_short_channel_id(struct json_stream *response UNNEEDED, const char *fieldname UNNEEDED, struct short_channel_id id UNNEEDED) { fprintf(stderr, "json_add_short_channel_id called!\n"); abort(); } +/* Generated stub for json_add_short_channel_id_dir */ +void json_add_short_channel_id_dir(struct json_stream *response UNNEEDED, + const char *fieldname UNNEEDED, + struct short_channel_id_dir idd UNNEEDED) +{ fprintf(stderr, "json_add_short_channel_id_dir called!\n"); abort(); } /* Generated stub for json_add_string */ void json_add_string(struct json_stream *js UNNEEDED, const char *fieldname UNNEEDED, @@ -212,6 +219,10 @@ bool json_to_sat(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, bool json_to_short_channel_id(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, struct short_channel_id *scid UNNEEDED) { fprintf(stderr, "json_to_short_channel_id called!\n"); abort(); } +/* Generated stub for json_to_short_channel_id_dir */ +bool json_to_short_channel_id_dir(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + struct short_channel_id_dir *scidd UNNEEDED) +{ fprintf(stderr, "json_to_short_channel_id_dir called!\n"); abort(); } /* Generated stub for json_to_u16 */ bool json_to_u16(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, uint16_t *num UNNEEDED) @@ -415,6 +426,7 @@ int main(int argc, char *argv[]) struct payment_modifier **mods; char gossip_version = 10; char *gossipfilename; + struct channel_hint_set *hints = channel_hint_set_new(tmpctx); common_setup(argv[0]); chainparams = chainparams_for_network("regtest"); @@ -431,7 +443,7 @@ int main(int argc, char *argv[]) } mods = tal_arrz(tmpctx, struct payment_modifier *, 1); - p = payment_new(mods, tal(tmpctx, struct command), NULL, mods); + p = payment_new(mods, tal(tmpctx, struct command), NULL, hints, mods); for (size_t i = 1; i < NUM_NODES; i++) { struct short_channel_id scid; @@ -480,6 +492,7 @@ int main(int argc, char *argv[]) assert(tal_count(r) == 2); } + tal_free(hints); common_shutdown(); return 0; } diff --git a/tests/test_pay.py b/tests/test_pay.py index ab028c35f..86bccdcf2 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -5998,7 +5998,7 @@ def test_enableoffer(node_factory): l1.rpc.enableoffer(offer_id=offer1['offer_id']) -def test_pay_remember_hint(node_factory): +def diamond_network(node_factory): """Build a diamond, with a cheap route, that is exhausted. The first payment should try that route first, learn it's exhausted, and then succeed over the other leg. The second, unrelated, @@ -6007,9 +6007,68 @@ def test_pay_remember_hint(node_factory): ```mermaid graph LR - Sender -- "propfee=1\nexhausted" --> Forwarder1 - Forwarder1 -- "propfee=1" --> Recipient - Sender -- "propfee=50" --> Forwarder2 - Forwarder2 -- "propfee=1" --> Recipient + Sender -- "propfee=1" --> Forwarder1 + Forwarder1 -- "propfee="1\nexhausted --> Recipient + Sender -- "propfee=1" --> Forwarder2 + Forwarder2 -- "propfee=5" --> Recipient ``` """ + opts = [ + {'fee-per-satoshi': 0, 'fee-base': 0}, # Sender + {'fee-per-satoshi': 0, 'fee-base': 0}, # Low fee, but exhausted channel + {'fee-per-satoshi': 5000, 'fee-base': 0}, # Disincentivize using fw2 + {'fee-per-satoshi': 0, 'fee-base': 0}, # Recipient + ] + + sender, fw1, fw2, recipient, = node_factory.get_nodes(4, opts=opts) + + # And now wire them all up: notice that all channels, except the + # recipent <> fw1 are created in the direction of the planned + # from, meaning we won't be able to forward through there, causing + # a `channel_hint` to be created, disincentivizing usage of this + # channel on the second payment. + node_factory.join_nodes( + [sender, fw2, recipient, fw1], + wait_for_announce=True, + announce_channels=True, + ) + # And we complete the diamond by adding the edge from sender to fw1 + node_factory.join_nodes( + [sender, fw1], + wait_for_announce=True, + announce_channels=True + ) + return [sender, fw1, fw2, recipient] + + +def test_pay_remember_hint(node_factory): + """Using a diamond graph, with inferred `channel_hint`s, see if we remember + """ + sender, fw1, fw2, recipient, = diamond_network(node_factory) + + inv = recipient.rpc.invoice( + 4200000, + "lbl1", + "desc1", + exposeprivatechannels=[], # suppress routehints, so fees alone control route + )['bolt11'] + + p = sender.rpc.pay(inv) + + # Ensure we failed the first, cheap, path, and then tried the successful one. + assert(p['parts'] == 2) + + # Now for the final trick: a new payment should remember the + # previous failure, and go directly for the successful route + # through fw2 + + inv = recipient.rpc.invoice( + 4200000, + "lbl2", + "desc2", + exposeprivatechannels=[], # suppress routehints, so fees alone control route + )['bolt11'] + + # We should not have touched fw1, and should succeed after a single call + p = sender.rpc.pay(inv) + assert(p['parts'] == 1)