diff --git a/common/gossmap.c b/common/gossmap.c index 068a25c41566..990cb421cbca 100644 --- a/common/gossmap.c +++ b/common/gossmap.c @@ -474,8 +474,8 @@ static struct gossmap_chan *add_channel(struct gossmap *map, return NULL; } - /* gossipd writes WIRE_GOSSIP_STORE_CHANNEL_AMOUNT after this (not for - * local channels), so ignore channel_announcement until that appears */ + /* gossipd writes WIRE_GOSSIP_STORE_CHANNEL_AMOUNT after this, + * so ignore channel_announcement until that appears */ if (msglen && (map->map_size < cannounce_off + msglen + sizeof(struct gossip_hdr) + sizeof(u16) + sizeof(u64))) return NULL; @@ -760,24 +760,34 @@ static void destroy_map(struct gossmap *map) } /* Local modifications. We only expect a few, so we use a simple - * array. */ + * array. If this changes, use a hashtable and a storage area for all + * those pointers to avoid dynamic allocation overhead! */ struct localmod { struct short_channel_id scid; /* If this is an entirely-local channel, here's its offset. * Otherwise, 0xFFFFFFFF. */ u32 local_off; - /* Are updates in either direction set? */ - bool updates_set[2]; - /* hc[n] defined if updates_set[n]. */ - struct half_chan hc[2]; - /* orig[n] defined if updates_set[n] and local_off == 0xFFFFFFFF */ + /* Non-NULL values mean change existing ones */ + struct localmod_changes { + const bool *enabled; + const fp16_t *htlc_min, *htlc_max; + const u32 *base_fee, *proportional_fee; + const u16 *delay; + } changes[2]; + + /* orig[n] defined if local_off == 0xFFFFFFFF */ struct half_chan orig[2]; /* Original update offsets */ u32 orig_cupdate_off[2]; }; +static bool localmod_is_local_chan(const struct localmod *mod) +{ + return mod->local_off != 0xFFFFFFFF; +} + struct gossmap_localmods { struct localmod *mods; /* This is the local array to be used by the gossmap */ @@ -818,6 +828,7 @@ bool gossmap_local_addchan(struct gossmap_localmods *localmods, const struct node_id *n1, const struct node_id *n2, struct short_channel_id scid, + struct amount_msat capacity, const u8 *features) { be16 be16; @@ -837,15 +848,23 @@ bool gossmap_local_addchan(struct gossmap_localmods *localmods, * compressed keys sorted in ascending lexicographic order. */ if (node_id_cmp(n1, n2) > 0) - return gossmap_local_addchan(localmods, n2, n1, scid, features); + return gossmap_local_addchan(localmods, n2, n1, scid, capacity, + features); mod.scid = scid; - mod.updates_set[0] = mod.updates_set[1] = false; + memset(&mod.changes, 0, sizeof(mod.changes)); - /* We create fake local channel_announcement. */ + /* We create amount, then fake local channel_announcement */ off = insert_local_space(localmods, - 2 + 64 * 4 + 2 + tal_bytelen(features) + 8 + 2 + 64 * 4 + 2 + tal_bytelen(features) + 32 + 8 + 33 + 33); + + /* Write amount */ + be64 = be64_to_cpu(capacity.millisatoshis / 1000 /* Raw: gossmap */); + memcpy(localmods->local + off, &be64, sizeof(be64)); + off += sizeof(be64); + + /* From here is a channel-announcment, with only the fields we use */ mod.local_off = off; /* Set type to be kosher. */ @@ -885,53 +904,100 @@ bool gossmap_local_addchan(struct gossmap_localmods *localmods, return true; }; -/* Insert a local-only channel_update. */ +/* Insert a local-only channel_update: false if can't represent. */ bool gossmap_local_updatechan(struct gossmap_localmods *localmods, - struct short_channel_id scid, - struct amount_msat htlc_min, - struct amount_msat htlc_max, - u32 base_fee, - u32 proportional_fee, - u16 delay, - bool enabled, - int dir) + const struct short_channel_id_dir *scidd, + const bool *enabled, + const struct amount_msat *htlc_min, + const struct amount_msat *htlc_max, + const struct amount_msat *base_fee, + const u32 *proportional_fee, + const u16 *delay) { struct localmod *mod; + struct localmod_changes *lc; + struct half_chan test; + + /* Check fit before making any changes. */ + if (base_fee) { + test.base_fee = base_fee->millisatoshis /* Raw: localmod */; + if (!amount_msat_eq(amount_msat(test.base_fee), *base_fee)) + return false; + } + if (proportional_fee) { + test.proportional_fee = *proportional_fee; + if (test.proportional_fee != *proportional_fee) + return false; + } + if (delay) { + test.delay = *delay; + if (test.delay != *delay) + return false; + } - mod = find_localmod(localmods, scid); + mod = find_localmod(localmods, scidd->scid); if (!mod) { /* Create new reference to (presumably) existing channel. */ size_t nmods = tal_count(localmods->mods); tal_resize(&localmods->mods, nmods + 1); mod = &localmods->mods[nmods]; - mod->scid = scid; - mod->updates_set[0] = mod->updates_set[1] = false; + mod->scid = scidd->scid; + memset(&mod->changes, 0, sizeof(mod->changes)); mod->local_off = 0xFFFFFFFF; } - assert(dir == 0 || dir == 1); - mod->updates_set[dir] = true; - mod->hc[dir].enabled = enabled; - /* node_idx needs to be set once we're in the gossmap. */ - mod->hc[dir].htlc_min - = u64_to_fp16(htlc_min.millisatoshis, /* Raw: to fp16 */ - false); - mod->hc[dir].htlc_max - = u64_to_fp16(htlc_max.millisatoshis, /* Raw: to fp16 */ - true); - mod->hc[dir].base_fee = base_fee; - mod->hc[dir].proportional_fee = proportional_fee; - mod->hc[dir].delay = delay; - - /* Check they fit */ - if (mod->hc[dir].base_fee != base_fee - || mod->hc[dir].proportional_fee != proportional_fee - || mod->hc[dir].delay != delay) - return false; + lc = &mod->changes[scidd->dir]; + if (enabled) { + tal_free(lc->enabled); + lc->enabled = tal_dup(localmods, bool, enabled); + } + if (htlc_min) { + fp16_t min = u64_to_fp16(htlc_min->millisatoshis, /* Raw: to fp16 */ + false); + tal_free(lc->htlc_min); + lc->htlc_min = tal_dup(localmods, fp16_t, &min); + } + if (htlc_max) { + fp16_t max = u64_to_fp16(htlc_max->millisatoshis, /* Raw: to fp16 */ + true); + tal_free(lc->htlc_max); + lc->htlc_max = tal_dup(localmods, fp16_t, &max); + } + if (base_fee) { + u32 base_as_u32 = base_fee->millisatoshis; /* Raw: localmod */ + tal_free(lc->base_fee); + lc->base_fee = tal_dup(localmods, u32, &base_as_u32); + } + if (proportional_fee) { + tal_free(lc->proportional_fee); + lc->proportional_fee = tal_dup(localmods, u32, proportional_fee); + } + if (delay) { + tal_free(lc->delay); + lc->delay = tal_dup(localmods, u16, delay); + } return true; } +bool gossmap_local_setchan(struct gossmap_localmods *localmods, + struct short_channel_id scid, + struct amount_msat htlc_min, + struct amount_msat htlc_max, + struct amount_msat base_fee, + u32 proportional_fee, + u16 delay, + bool enabled, + int dir) +{ + struct short_channel_id_dir scidd = {scid, dir}; + return gossmap_local_updatechan(localmods, &scidd, + &enabled, + &htlc_min, &htlc_max, + &base_fee, &proportional_fee, + &delay); +} + /* Apply localmods to this map */ void gossmap_apply_localmods(struct gossmap *map, struct gossmap_localmods *localmods) @@ -949,22 +1015,56 @@ void gossmap_apply_localmods(struct gossmap *map, chan = gossmap_find_chan(map, &mod->scid); /* If it doesn't exist, are we supposed to create a local one? */ if (!chan) { - if (mod->local_off == 0xFFFFFFFF) + if (!localmod_is_local_chan(mod)) continue; /* Create new channel, pointing into local. */ chan = add_channel(map, map->map_size + mod->local_off, 0); } - /* Save old, overwrite (keep nodeidx) */ + /* Save old, update any fields they wanted to change */ for (size_t h = 0; h < 2; h++) { - if (!mod->updates_set[h]) - continue; + bool was_set, all_changed; + const struct localmod_changes *c = &mod->changes[h]; + /* Save existing versions */ mod->orig[h] = chan->half[h]; mod->orig_cupdate_off[h] = chan->cupdate_off[h]; - chan->half[h] = mod->hc[h]; - chan->half[h].nodeidx = mod->orig[h].nodeidx; - chan->cupdate_off[h] = 0xFFFFFFFF; + + was_set = gossmap_chan_set(chan, h); + + /* Override specified fields. */ + all_changed = true; + if (c->enabled) + chan->half[h].enabled = *c->enabled; + else + all_changed = false; + if (c->htlc_min) + chan->half[h].htlc_min = *c->htlc_min; + else + all_changed = false; + if (c->htlc_max) + chan->half[h].htlc_max = *c->htlc_max; + else + all_changed = false; + if (c->base_fee) + chan->half[h].base_fee = *c->base_fee; + else + all_changed = false; + if (c->proportional_fee) + chan->half[h].proportional_fee = *c->proportional_fee; + else + all_changed = false; + if (c->delay) + chan->half[h].delay = *c->delay; + else + all_changed = false; + + /* Is it all defined? + * This controls gossmap_chan_set(chan, h); */ + if (was_set || all_changed) + chan->cupdate_off[h] = 0xFFFFFFFF; + else + chan->cupdate_off[h] = 0; } } } @@ -980,19 +1080,17 @@ void gossmap_remove_localmods(struct gossmap *map, const struct localmod *mod = &localmods->mods[i]; struct gossmap_chan *chan = gossmap_find_chan(map, &mod->scid); + /* If there was no channel, ignore */ + if (!chan) + continue; + /* If that's a local channel, remove it now. */ if (chan->cann_off >= map->map_size) { gossmap_remove_chan(map, chan); } else { - /* Restore (keep nodeidx). */ + /* Restore. */ for (size_t h = 0; h < 2; h++) { - u32 nodeidx; - if (!mod->updates_set[h]) - continue; - - nodeidx = chan->half[h].nodeidx; chan->half[h] = mod->orig[h]; - chan->half[h].nodeidx = nodeidx; chan->cupdate_off[h] = mod->orig_cupdate_off[h]; } } @@ -1135,34 +1233,39 @@ bool gossmap_chan_is_dying(const struct gossmap *map, return ghdr.flags & CPU_TO_BE16(GOSSIP_STORE_DYING_BIT); } -bool gossmap_chan_get_capacity(const struct gossmap *map, - const struct gossmap_chan *c, - struct amount_sat *amount) +struct amount_msat gossmap_chan_get_capacity(const struct gossmap *map, + const struct gossmap_chan *c) { struct gossip_hdr ghdr; size_t off; u16 type; + struct amount_sat sat; + struct amount_msat msat; - /* Fail for local channels */ - if (gossmap_chan_is_localmod(map, c)) - return false; + if (gossmap_chan_is_localmod(map, c)) { + /* Amount is *before* c->cann_off */ + off = c->cann_off - sizeof(u64); + goto get_amount; + } /* Skip over this record to next; expect a gossip_store_channel_amount */ off = c->cann_off - sizeof(ghdr); map_copy(map, off, &ghdr, sizeof(ghdr)); off += sizeof(ghdr) + be16_to_cpu(ghdr.len); - /* Partial write, this can happen. */ - if (off + sizeof(ghdr) + sizeof(u16) + sizeof(u64) > map->map_size) - return false; - - /* Get type of next field. */ + /* We don't allow loading a channel unless it has capacity field! */ type = map_be16(map, off + sizeof(ghdr)); - if (type != WIRE_GOSSIP_STORE_CHANNEL_AMOUNT) - return false; + assert(type == WIRE_GOSSIP_STORE_CHANNEL_AMOUNT); - *amount = amount_sat(map_be64(map, off + sizeof(ghdr) + sizeof(be16))); - return true; + off += sizeof(ghdr) + sizeof(be16); + +get_amount: + /* Shouldn't overflow */ + sat = amount_sat(map_be64(map, off)); + if (!amount_sat_to_msat(&msat, sat)) + errx(1, "invalid capacity %s", fmt_amount_sat(tmpctx, sat)); + + return msat; } struct gossmap_chan *gossmap_nth_chan(const struct gossmap *map, diff --git a/common/gossmap.h b/common/gossmap.h index ade384f16465..6a4a5308a924 100644 --- a/common/gossmap.h +++ b/common/gossmap.h @@ -94,22 +94,33 @@ bool gossmap_local_addchan(struct gossmap_localmods *localmods, const struct node_id *n1, const struct node_id *n2, struct short_channel_id scid, + struct amount_msat capacity, const u8 *features) NON_NULL_ARGS(1,2,3); /* Create a local-only channel_update: can apply to lcoal-only or * normal channels. Returns false if amounts don't fit in our - * internal representation (implies channel unusable anyway). */ + * internal representation (implies channel unusable anyway). Any + * NULL arguments mean "leave as is". */ bool gossmap_local_updatechan(struct gossmap_localmods *localmods, - struct short_channel_id scid, - struct amount_msat htlc_min, - struct amount_msat htlc_max, - u32 base_fee, - u32 proportional_fee, - u16 delay, - bool enabled, - int dir) - NO_NULL_ARGS; + const struct short_channel_id_dir *scidd, + const bool *enabled, + const struct amount_msat *htlc_min, + const struct amount_msat *htlc_max, + const struct amount_msat *base_fee, + const u32 *proportional_fee, + const u16 *delay); + +/* Convenience version which sets everything (older API) */ +bool gossmap_local_setchan(struct gossmap_localmods *localmods, + struct short_channel_id scid, + struct amount_msat htlc_min, + struct amount_msat htlc_max, + struct amount_msat base_fee, + u32 proportional_fee, + u16 delay, + bool enabled, + int dir); /* Apply localmods to this map */ void gossmap_apply_localmods(struct gossmap *map, @@ -161,10 +172,9 @@ static inline bool gossmap_chan_set(const struct gossmap_chan *chan, int dir) return chan->cupdate_off[dir] != 0; } -/* Return capacity if it's known (fails on a local mod) */ -bool gossmap_chan_get_capacity(const struct gossmap *map, - const struct gossmap_chan *c, - struct amount_sat *amount); +/* Return capacity (in msat). */ +struct amount_msat gossmap_chan_get_capacity(const struct gossmap *map, + const struct gossmap_chan *c); /* Get the announcement msg which created this chan (NULL for localmods) */ u8 *gossmap_chan_get_announce(const tal_t *ctx, diff --git a/common/gossmods_listpeerchannels.c b/common/gossmods_listpeerchannels.c index 73864241302e..df0b97133132 100644 --- a/common/gossmods_listpeerchannels.c +++ b/common/gossmods_listpeerchannels.c @@ -9,12 +9,13 @@ void gossmod_add_localchan(struct gossmap_localmods *mods, const struct node_id *self, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf UNUSED, const jsmntok_t *chantok UNUSED, @@ -26,14 +27,15 @@ void gossmod_add_localchan(struct gossmap_localmods *mods, max = spendable; /* FIXME: features? */ - gossmap_local_addchan(mods, self, peer, scidd->scid, NULL); - - gossmap_local_updatechan(mods, scidd->scid, min, max, - fee_base.millisatoshis, /* Raw: gossmap */ - fee_proportional, - cltv_delta, - enabled, - scidd->dir); + gossmap_local_addchan(mods, self, peer, scidd->scid, capacity_msat, + NULL); + + gossmap_local_updatechan(mods, scidd, + &enabled, + &min, &max, + &fee_base, + &fee_proportional, + &cltv_delta); } struct gossmap_localmods * @@ -46,12 +48,13 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, const struct node_id *self, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat sr_able, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf, const jsmntok_t *chantok, @@ -68,7 +71,7 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, struct short_channel_id alias; bool enabled; struct node_id dst; - struct amount_msat spendable, receivable, fee_base[NUM_SIDES], htlc_min[NUM_SIDES], htlc_max[NUM_SIDES]; + struct amount_msat capacity_msat, spendable, receivable, fee_base[NUM_SIDES], htlc_min[NUM_SIDES], htlc_max[NUM_SIDES]; u32 fee_proportional[NUM_SIDES], cltv_delta[NUM_SIDES]; const char *state, *err; @@ -87,6 +90,7 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, "peer_connected:%," "state:%," "peer_id:%," + "total_msat?:%," "updates?:{" "local" ":{fee_base_msat:%," @@ -108,6 +112,7 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, JSON_SCAN(json_to_bool, &enabled), JSON_SCAN_TAL(tmpctx, json_strdup, &state), JSON_SCAN(json_to_node_id, &dst), + JSON_SCAN(json_to_msat, &capacity_msat), JSON_SCAN(json_to_msat, &fee_base[LOCAL]), JSON_SCAN(json_to_u32, &fee_proportional[LOCAL]), JSON_SCAN(json_to_msat, &htlc_min[LOCAL]), @@ -148,7 +153,8 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, } /* We add both directions */ - cb(mods, self, &dst, &scidd, htlc_min[LOCAL], htlc_max[LOCAL], + cb(mods, self, &dst, &scidd, capacity_msat, + htlc_min[LOCAL], htlc_max[LOCAL], spendable, fee_base[LOCAL], fee_proportional[LOCAL], cltv_delta[LOCAL], enabled, buf, channel, cbarg); @@ -158,7 +164,8 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, scidd.dir = !scidd.dir; - cb(mods, self, &dst, &scidd, htlc_min[REMOTE], htlc_max[REMOTE], + cb(mods, self, &dst, &scidd, capacity_msat, + htlc_min[REMOTE], htlc_max[REMOTE], receivable, fee_base[REMOTE], fee_proportional[REMOTE], cltv_delta[REMOTE], enabled, buf, channel, cbarg); } diff --git a/common/gossmods_listpeerchannels.h b/common/gossmods_listpeerchannels.h index a9847c882645..22eafdd3d143 100644 --- a/common/gossmods_listpeerchannels.h +++ b/common/gossmods_listpeerchannels.h @@ -30,12 +30,13 @@ struct gossmap_localmods *gossmods_from_listpeerchannels_(const tal_t *ctx, const struct node_id *self_, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf_, const jsmntok_t *chantok, @@ -53,8 +54,9 @@ struct gossmap_localmods *gossmods_from_listpeerchannels_(const tal_t *ctx, struct amount_msat, \ struct amount_msat, \ struct amount_msat, \ + struct amount_msat, \ u32, \ - u32, \ + u16, \ bool, \ const char *, \ const jsmntok_t *), \ @@ -65,12 +67,13 @@ void gossmod_add_localchan(struct gossmap_localmods *mods, const struct node_id *self, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf UNUSED, const jsmntok_t *chantok UNUSED, diff --git a/common/json_param.c b/common/json_param.c index 3a25e1684b2f..216451271803 100644 --- a/common/json_param.c +++ b/common/json_param.c @@ -642,6 +642,19 @@ struct command_result *param_short_channel_id(struct command *cmd, "should be a short_channel_id of form NxNxN"); } +struct command_result *param_short_channel_id_dir(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct short_channel_id_dir **scidd) +{ + *scidd = tal(cmd, struct short_channel_id_dir); + if (!json_to_short_channel_id_dir(buffer, tok, *scidd)) + return command_fail_badparam(cmd, name, buffer, tok, + "should be a short_channel_id_dir of form NxNxN/N"); + return NULL; +} + struct command_result *param_secret(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, struct secret **secret) diff --git a/common/json_param.h b/common/json_param.h index 83e4f3b3a70a..07db22f3d2cf 100644 --- a/common/json_param.h +++ b/common/json_param.h @@ -275,6 +275,12 @@ struct command_result *param_short_channel_id(struct command *cmd, const jsmntok_t *tok, struct short_channel_id **scid); +struct command_result *param_short_channel_id_dir(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct short_channel_id_dir **scidd); + /* Ignore the token. Not usually used. */ struct command_result *param_ignore(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, diff --git a/common/json_parse.c b/common/json_parse.c index 639c35b2e70b..1d272b1f0c6c 100644 --- a/common/json_parse.c +++ b/common/json_parse.c @@ -109,6 +109,17 @@ bool json_to_int(const char *buffer, const jsmntok_t *tok, int *num) return true; } +bool json_to_zero_or_one(const char *buffer, const jsmntok_t *tok, int *num) +{ + u32 v32; + if (!json_to_u32(buffer, tok, &v32)) + return false; + if (v32 != 0 && v32 != 1) + return false; + *num = v32; + return true; +} + bool json_to_jsonrpc_errcode(const char *buffer, const jsmntok_t *tok, enum jsonrpc_errcode *errcode) { @@ -577,7 +588,6 @@ bool json_to_short_channel_id_dir(const char *buffer, const jsmntok_t *tok, struct short_channel_id_dir *scidd) { jsmntok_t scidtok, numtok; - u32 dir; if (!split_tok(buffer, tok, '/', &scidtok, &numtok)) return false; @@ -585,10 +595,9 @@ bool json_to_short_channel_id_dir(const char *buffer, const jsmntok_t *tok, if (!json_to_short_channel_id(buffer, &scidtok, &scidd->scid)) return false; - if (!json_to_u32(buffer, &numtok, &dir) || (dir > 1)) + if (!json_to_zero_or_one(buffer, &numtok, &scidd->dir)) return false; - scidd->dir = dir; return true; } diff --git a/common/json_parse.h b/common/json_parse.h index 7eca7284c2d6..4bd23cbddcd7 100644 --- a/common/json_parse.h +++ b/common/json_parse.h @@ -31,6 +31,9 @@ u8 *json_tok_bin_from_hex(const tal_t *ctx, const char *buffer, const jsmntok_t bool json_to_number(const char *buffer, const jsmntok_t *tok, unsigned int *num); +/* Extract 0/1 from this */ +bool json_to_zero_or_one(const char *buffer, const jsmntok_t *tok, int *num); + /* Extract signed 64 bit integer from this (may be a string, or a number literal) */ bool json_to_s64(const char *buffer, const jsmntok_t *tok, s64 *num); @@ -86,6 +89,10 @@ bool json_to_bitcoin_amount(const char *buffer, const jsmntok_t *tok, bool json_to_short_channel_id(const char *buffer, const jsmntok_t *tok, struct short_channel_id *scid); +/* Extract a short_channel_id_dir from this */ +bool json_to_short_channel_id_dir(const char *buffer, const jsmntok_t *tok, + struct short_channel_id_dir *scidd); + /* Extract a satoshis amount from this */ bool json_to_sat(const char *buffer, const jsmntok_t *tok, struct amount_sat *sat); diff --git a/common/test/run-gossmap_canned.c b/common/test/run-gossmap_canned.c index 1b267f65737d..e7f0662284cd 100644 --- a/common/test/run-gossmap_canned.c +++ b/common/test/run-gossmap_canned.c @@ -314,7 +314,7 @@ int main(int argc, char *argv[]) struct gossmap *map; struct node_id l1, l2; struct short_channel_id scid12; - struct amount_sat capacity; + struct amount_msat capacity; u32 timestamp, fee_base_msat, fee_proportional_millionths; u8 message_flags, channel_flags; struct amount_msat htlc_minimum_msat, htlc_maximum_msat; @@ -337,9 +337,8 @@ int main(int argc, char *argv[]) assert(short_channel_id_from_str("110x1x0", 7, &scid12)); assert(gossmap_find_chan(map, &scid12)); assert(!gossmap_chan_is_localmod(map, gossmap_find_chan(map, &scid12))); - assert(gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid12), - &capacity)); - assert(amount_sat_eq(capacity, AMOUNT_SAT(1000000))); + capacity = gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid12)); + assert(amount_msat_eq_sat(capacity, AMOUNT_SAT(1000000))); gossmap_chan_get_update_details(map, gossmap_find_chan(map, &scid12), 0, diff --git a/common/test/run-gossmap_local.c b/common/test/run-gossmap_local.c index 4debfc47c418..b583495c0439 100644 --- a/common/test/run-gossmap_local.c +++ b/common/test/run-gossmap_local.c @@ -333,10 +333,10 @@ int main(int argc, char *argv[]) char *gossfile; struct gossmap *map; struct node_id l1, l2, l3, l4; - struct short_channel_id scid23, scid12, scid_local; + struct short_channel_id scid23, scid12, scid_local, scid_nonexisting; struct gossmap_chan *chan; struct gossmap_localmods *mods; - struct amount_sat capacity; + struct amount_msat capacity; u32 timestamp, fee_base_msat, fee_proportional_millionths; u8 message_flags, channel_flags; struct amount_msat htlc_minimum_msat, htlc_maximum_msat; @@ -365,12 +365,10 @@ int main(int argc, char *argv[]) assert(!gossmap_chan_is_localmod(map, gossmap_find_chan(map, &scid23))); assert(gossmap_find_chan(map, &scid12)); assert(!gossmap_chan_is_localmod(map, gossmap_find_chan(map, &scid12))); - assert(gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid23), - &capacity)); - assert(amount_sat_eq(capacity, AMOUNT_SAT(1000000))); - assert(gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid12), - &capacity)); - assert(amount_sat_eq(capacity, AMOUNT_SAT(1000000))); + capacity = gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid23)); + assert(amount_msat_eq_sat(capacity, AMOUNT_SAT(1000000))); + capacity = gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid12)); + assert(amount_msat_eq_sat(capacity, AMOUNT_SAT(1000000))); gossmap_chan_get_update_details(map, gossmap_find_chan(map, &scid23), 0, @@ -467,7 +465,7 @@ int main(int argc, char *argv[]) assert(node_id_from_hexstr("0382ce59ebf18be7d84677c2e35f23294b9992ceca95491fcf8a56c6cb2d9de199", 66, &l4)); assert(short_channel_id_from_str("111x1x1", 7, &scid_local)); - assert(gossmap_local_addchan(mods, &l1, &l4, scid_local, NULL)); + assert(gossmap_local_addchan(mods, &l1, &l4, scid_local, AMOUNT_MSAT(100000), NULL)); /* Apply changes, check they work. */ gossmap_apply_localmods(map, mods); @@ -478,6 +476,10 @@ int main(int argc, char *argv[]) assert(!gossmap_chan_set(chan, 0)); assert(!gossmap_chan_set(chan, 1)); + /* Capacity is correct */ + assert(amount_msat_eq(gossmap_chan_get_capacity(map, chan), + AMOUNT_MSAT(100000))); + /* Remove, no longer can find. */ gossmap_remove_localmods(map, mods); @@ -485,18 +487,25 @@ int main(int argc, char *argv[]) assert(!gossmap_find_node(map, &l4)); /* Now update it both local, and an existing one. */ - gossmap_local_updatechan(mods, scid_local, - AMOUNT_MSAT(1), - AMOUNT_MSAT(100000), - 2, 3, 4, true, 0); + gossmap_local_setchan(mods, scid_local, + AMOUNT_MSAT(1), + AMOUNT_MSAT(100000), + AMOUNT_MSAT(2), 3, 4, true, 0); /* Adding an existing channel is a noop. */ - assert(gossmap_local_addchan(mods, &l2, &l3, scid23, NULL)); + assert(gossmap_local_addchan(mods, &l2, &l3, scid23, AMOUNT_MSAT(100000), NULL)); + + gossmap_local_setchan(mods, scid23, + AMOUNT_MSAT(99), + AMOUNT_MSAT(100), + AMOUNT_MSAT(101), 102, 103, true, 0); - gossmap_local_updatechan(mods, scid23, - AMOUNT_MSAT(99), - AMOUNT_MSAT(100), - 101, 102, 103, true, 0); + /* We can "update" a channel which doesn't exist, and it's a noop */ + scid_nonexisting.u64 = 1; + gossmap_local_setchan(mods, scid_nonexisting, + AMOUNT_MSAT(1), + AMOUNT_MSAT(100000), + AMOUNT_MSAT(2), 3, 4, false, 0); gossmap_apply_localmods(map, mods); chan = gossmap_find_chan(map, &scid_local); @@ -510,6 +519,8 @@ int main(int argc, char *argv[]) assert(chan->half[0].proportional_fee == 3); assert(chan->half[0].delay == 4); + assert(!gossmap_find_chan(map, &scid_nonexisting)); + chan = gossmap_find_chan(map, &scid23); assert(chan->half[0].enabled); assert(chan->half[0].htlc_min == u64_to_fp16(99, false)); diff --git a/common/test/run-json_filter.c b/common/test/run-json_filter.c index d4e68801a637..54138cbe6999 100644 --- a/common/test/run-json_filter.c +++ b/common/test/run-json_filter.c @@ -120,6 +120,10 @@ bool json_to_pubkey(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_txid */ bool json_to_txid(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, struct bitcoin_txid *txid UNNEEDED) diff --git a/common/test/run-json_remove.c b/common/test/run-json_remove.c index 93c844143415..f1c16a28cbe4 100644 --- a/common/test/run-json_remove.c +++ b/common/test/run-json_remove.c @@ -146,6 +146,10 @@ bool json_to_pubkey(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_txid */ bool json_to_txid(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, struct bitcoin_txid *txid UNNEEDED) diff --git a/common/test/run-route-infloop.c b/common/test/run-route-infloop.c index f4c3f9071f7a..bec98e232486 100644 --- a/common/test/run-route-infloop.c +++ b/common/test/run-route-infloop.c @@ -56,15 +56,13 @@ static double capacity_bias(const struct gossmap *map, int dir, struct amount_msat amount) { - struct amount_sat capacity; + struct amount_msat msat; u64 amtmsat = amount.millisatoshis; /* Raw: lengthy math */ double capmsat; - /* Can fail in theory if gossmap changed underneath. */ - if (!gossmap_chan_get_capacity(map, c, &capacity)) - return 0; + msat = gossmap_chan_get_capacity(map, c); - capmsat = (double)capacity.satoshis * 1000; /* Raw: lengthy math */ + capmsat = (double)msat.millisatoshis; /* Raw: log */ return -log((capmsat + 1 - amtmsat) / (capmsat + 1)); } @@ -138,17 +136,18 @@ int main(int argc, char *argv[]) /* We overlay our own channels as zero fee & delay, since we don't pay fees */ struct gossmap_localmods *localmods = gossmap_localmods_new(gossmap); for (size_t i = 0; i < me->num_chans; i++) { - int dir; - struct short_channel_id scid; - struct gossmap_chan *c = gossmap_nth_chan(gossmap, me, i, &dir); + struct short_channel_id_dir scidd; + const struct amount_msat base_fee = AMOUNT_MSAT(0); + const u32 proportional_fee = 0; + struct gossmap_chan *c = gossmap_nth_chan(gossmap, me, i, &scidd.dir); - if (!c->half[dir].enabled) + if (!c->half[scidd.dir].enabled) continue; - scid = gossmap_chan_scid(gossmap, c); - assert(gossmap_local_updatechan(localmods, scid, - amount_msat(fp16_to_u64(c->half[dir].htlc_min)), - amount_msat(fp16_to_u64(c->half[dir].htlc_max)), - 0, 0, 0, true, dir)); + scidd.scid = gossmap_chan_scid(gossmap, c); + assert(gossmap_local_updatechan(localmods, &scidd, + NULL, NULL, NULL, + &base_fee, &proportional_fee, + NULL)); } gossmap_apply_localmods(gossmap, localmods); @@ -173,13 +172,13 @@ int main(int argc, char *argv[]) } else { double probability = 1; for (size_t j = 0; j < tal_count(r); j++) { - struct amount_sat capacity_sat; + struct amount_msat msat; u64 cap_msat; struct gossmap_chan *c = gossmap_find_chan(gossmap, &r[j].scid); assert(c); - assert(gossmap_chan_get_capacity(gossmap, c, &capacity_sat)); + msat = gossmap_chan_get_capacity(gossmap, c); - cap_msat = capacity_sat.satoshis * 1000; + cap_msat = msat.millisatoshis; /* Assume linear distribution, implying probability depends on * amount we would leave in channel */ assert(cap_msat >= r[0].amount.millisatoshis); diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index a9b6d1572ac4..7db584550643 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -265,7 +265,7 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-create-channel** RPC command tells askrene to populate one direction of a channel in the given layer. If the channel already exists, it will be overridden. If the layer does not exist, it will be created." + "The **askrene-create-channel** RPC command tells askrene create a channel in the given layer. To actually populate the channel use *askrene-update-channel* in each direction." ], "request": { "required": [ @@ -273,12 +273,7 @@ "source", "destination", "short_channel_id", - "capacity_msat", - "htlc_min", - "htlc_max", - "base_fee", - "proportional_fee", - "delay" + "capacity_msat" ], "properties": { "layer": { @@ -308,37 +303,8 @@ "capacity_msat": { "type": "msat", "description": [ - "The capacity (onchain size) of the channel." - ] - }, - "htlc_min": { - "type": "msat", - "description": [ - "The minimum value allowed in this direction." - ] - }, - "htlc_max": { - "type": "msat", - "description": [ - "The maximum value allowed in this direction." - ] - }, - "base_fee": { - "type": "msat", - "description": [ - "The base fee to apply to use the channel in this direction." - ] - }, - "proportional_fee": { - "type": "u32", - "description": [ - "The proportional fee (in parts per million) to apply to use the channel in this direction." - ] - }, - "delay": { - "type": "u16", - "description": [ - "The CLTV delay required for this direction." + "The capacity (onchain size) of the channel.", + "NOTE: this is in millisatoshis!" ] } } @@ -350,6 +316,208 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-disable-node(7)", + "lightning-askrene-update-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, + "lightning-askrene-create-layer.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-create-layer", + "title": "Command to create a new layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-create-layer** RPC command tells askrene to create a new, empty layer. This layer can then be populated with `askrene-create-channel` and `askrene-inform-channel`, and be used in `getroutes`." + ], + "request": { + "required": [ + "layer" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to create." + ] + } + } + }, + "response": { + "required": [ + "layers" + ], + "properties": { + "layers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "layer", + "disabled_nodes", + "created_channels", + "channel_updates", + "constraints" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer." + ] + }, + "disabled_nodes": { + "type": "array", + "items": { + "type": "pubkey", + "description": [ + "The id of the disabled node." + ] + } + }, + "disabled_channels": { + "type": "array", + "items": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction which is disabled." + ] + } + }, + "created_channels": { + "type": "array", + "items": { + "type": "object", + "required": [ + "source", + "destination", + "short_channel_id", + "capacity_msat" + ], + "properties": { + "source": { + "type": "pubkey", + "description": [ + "The source node id for the channel." + ] + }, + "destination": { + "type": "pubkey", + "description": [ + "The destination node id for the channel." + ] + }, + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id for the channel." + ] + }, + "capacity_msat": { + "type": "msat", + "description": [ + "The capacity (onchain size) of the channel." + ] + } + } + } + }, + "channel_updates": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { + "htlc_minimum_msat": { + "type": "msat", + "description": [ + "The minimum value allowed in this direction." + ] + }, + "htlc_maximum_msat": { + "type": "msat", + "description": [ + "The maximum value allowed in this direction." + ] + }, + "fee_base_msat": { + "type": "msat", + "description": [ + "The base fee to apply to use the channel in this direction." + ] + }, + "fee_proportional_millionths": { + "type": "u32", + "description": [ + "The proportional fee (in parts per million) to apply to use the channel in this direction." + ] + }, + "delay": { + "type": "u16", + "description": [ + "The CLTV delay required for this direction." + ] + } + } + } + }, + "constraints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id", + "direction" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id." + ] + }, + "direction": { + "type": "u32", + "description": [ + "The direction." + ] + }, + "maximum_msat": { + "type": "msat", + "description": [ + "The maximum value which this channel could pass. This or *minimum_msat* will be present, but not both." + ] + }, + "minimum_msat": { + "type": "msat", + "description": [ + "The minimum value which this channel could pass. This or *minimum_msat* will be present, but not both." + ] + } + } + } + } + } + } + } + } + }, + "see_also": [ + "lightning-askrene-remove-layer(7)", + "lightning-getroutes(7)", + "lightning-askrene-create-channel(7)", "lightning-askrene-inform-channel(7)", "lightning-askrene-listlayers(7)", "lightning-askrene-age(7)" @@ -419,13 +587,14 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-inform-channel** RPC command tells askrene about the minimum or maximum current capacity of a given channel. It can be applied whether the curren channel exists or not. If the layer does not exist, it will be created." + "The **askrene-inform-channel** RPC command tells askrene about channels we used so it can update its capacity estimates. For most accuracy, you should remove your own reservations before calling this. It can be applied whether the current channel exists or not." ], "request": { "required": [ "layer", - "short_channel_id", - "direction" + "short_channel_id_dir", + "amount_msat", + "inform" ], "properties": { "layer": { @@ -434,74 +603,62 @@ "The name of the layer to apply this change to." ] }, - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The short channel id to apply this change to." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The direction to apply this change to." + "The short channel id and direction to apply this change to." ] }, - "minimum_msat": { + "amount_msat": { "type": "msat", "description": [ - "The minumum value which this channel could pass. This or *minimum_msat* must be specified, but not both." + "The amount we used on the channel" ] }, - "maximum_msat": { - "type": "msat", + "inform": { + "type": "string", + "enum": [ + "constrained", + "unconstrained", + "succeeded" + ], "description": [ - "The maximum value which this channel could pass. This or *minimum_msat* must be specified, but not both." + "Whether this payment passed (implying capacity of at least that amount), failed (implying maximum capacity of one msat less), or succeeded (implying capacity has been reduced in this direction)" ] } } }, "response": { "required": [ - "constraint" + "constraints" ], "properties": { - "constraint": { - "type": "object", - "required": [ - "short_channel_id", - "direction", - "timestamp" - ], - "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The *short_channel_id* specified." - ] - }, - "direction": { - "type": "u32", - "description": [ - "The *direction* specified." - ] - }, - "timestamp": { - "type": "u64", - "description": [ - "The UNIX time (seconds since 1970) this was created." - ] - }, - "maximum_msat": { - "type": "msat", - "description": [ - "The *minimum_msat* (if specified)" - ] - }, - "minimum_msat": { - "type": "msat", - "description": [ - "The *maximum_msat* (if specified)" - ] + "constraints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The short channel id and direction" + ] + }, + "maximum_msat": { + "type": "msat", + "description": [ + "The maximum value which this channel could pass." + ] + }, + "minimum_msat": { + "type": "msat", + "description": [ + "The minimum value which this channel could pass." + ] + } } } } @@ -557,6 +714,7 @@ "layer", "disabled_nodes", "created_channels", + "channel_updates", "constraints" ], "properties": { @@ -575,6 +733,15 @@ ] } }, + "disabled_channels": { + "type": "array", + "items": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction which is disabled." + ] + } + }, "created_channels": { "type": "array", "items": { @@ -583,12 +750,7 @@ "source", "destination", "short_channel_id", - "capacity_msat", - "htlc_minimum_msat", - "htlc_maximum_msat", - "fee_base_msat", - "fee_proportional_millionths", - "delay" + "capacity_msat" ], "properties": { "source": { @@ -614,7 +776,18 @@ "description": [ "The capacity (onchain size) of the channel." ] - }, + } + } + } + }, + "channel_updates": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { "htlc_minimum_msat": { "type": "msat", "description": [ @@ -653,32 +826,25 @@ "items": { "type": "object", "required": [ - "short_channel_id", - "direction" + "short_channel_id_dir" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The short channel id." - ] - }, - "direction": { - "type": "u32", - "description": [ - "The direction." + "The short channel id and direction" ] }, "maximum_msat": { "type": "msat", "description": [ - "The maximum value which this channel could pass. This or *minimum_msat* will be present, but not both." + "The maximum value which this channel could pass." ] }, "minimum_msat": { "type": "msat", "description": [ - "The minimum value which this channel could pass. This or *minimum_msat* will be present, but not both." + "The minimum value which this channel could pass." ] } } @@ -703,6 +869,117 @@ "Main web site: " ] }, + "lightning-askrene-listreservations.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-listreservations", + "title": "Command to display information about reservations (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-reservations** RPC command reports outstanding reservations made with `askrene-reserve`, mainly for debugging." + ], + "request": { + "required": [], + "properties": {} + }, + "response": { + "required": [ + "reservations" + ], + "properties": { + "layers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "short_channel_id_dir", + "amount_msat", + "age_in_seconds", + "command_id" + ], + "properties": { + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction that is reserved." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "The amount reserved." + ] + }, + "age_in_seconds": { + "type": "u64", + "description": [ + "The age of this reservation." + ] + }, + "command_id": { + "type": "string", + "description": [ + "The JSON id of the command used to make the reservation." + ] + } + } + } + } + } + }, + "see_also": [ + "lightning-askrene-reserve(7)", + "lightning-askrene-unreserve(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, + "lightning-askrene-remove-layer.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-remove-layer", + "title": "Command to destroy a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-remove-layer** RPC command tells askrene to forget a layer." + ], + "request": { + "required": [ + "layer" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to remove." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-askrene-create-layer(7)", + "lightning-askrene-listlayers(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, "lightning-askrene-reserve.json": { "$schema": "../rpc-schema-draft.json", "type": "object", @@ -712,7 +989,7 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-reserve** RPC command tells askrene that a path is being attempted. This allows it to take that into account when other *getroutes* calls are made. You should call **askrene-unreserve** after the attempt has completed.", + "The **askrene-reserve** RPC command tells askrene that a path is being attempted. This allows it to take that into account when other *getroutes* calls are made. You should call *askrene-unreserve* after the attempt has completed (and before calling *askrene-inform*).", "", "Note that additional properties inside the *path* elements are ignored, which is useful when used with the result of *getroutes*." ], @@ -727,21 +1004,14 @@ "type": "object", "additionalProperties": true, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "amount_msat" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", - "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { @@ -762,11 +1032,7 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-unreserve(7)", - "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", - "lightning-askrene-inform-channel(7)", - "lightning-askrene-listlayers(7)", - "lightning-askrene-age(7)" + "lightning-askrene-listreservations(7)" ], "author": [ "Rusty Russell <> is mainly responsible." @@ -799,21 +1065,14 @@ "type": "object", "additionalProperties": true, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "amount_msat" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { @@ -834,11 +1093,83 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-reserve(7)", - "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", - "lightning-askrene-inform-channel(7)", - "lightning-askrene-listlayers(7)", - "lightning-askrene-age(7)" + "lightning-askrene-listreservations(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, + "lightning-askrene-update-channel.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-update-channel", + "title": "Command to manipulate channel in a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-update-channel** RPC command overrides updates for an existing channel when the layer is applied." + ], + "request": { + "required": [ + "layer", + "short_channel_id_dir" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction to apply the change to." + ] + }, + "htlc_min": { + "type": "msat", + "description": [ + "The minimum value allowed in this direction." + ] + }, + "htlc_max": { + "type": "msat", + "description": [ + "The maximum value allowed in this direction." + ] + }, + "base_fee": { + "type": "msat", + "description": [ + "The base fee to apply to use the channel in this direction." + ] + }, + "proportional_fee": { + "type": "u32", + "description": [ + "The proportional fee (in parts per million) to apply to use the channel in this direction." + ] + }, + "cltv_expiry_delta": { + "type": "u16", + "description": [ + "The CLTV delay required for this direction." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-create-channel(7)" ], "author": [ "Rusty Russell <> is mainly responsible." @@ -14375,23 +14706,16 @@ "type": "object", "additionalProperties": false, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "next_node_id", "amount_msat", "delay" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { diff --git a/devtools/gossmap-compress.c b/devtools/gossmap-compress.c index 0aaf4046ed35..d41672a54b25 100644 --- a/devtools/gossmap-compress.c +++ b/devtools/gossmap-compress.c @@ -234,18 +234,17 @@ static u64 get_htlc_max(struct gossmap *gossmap, int dir) { struct amount_msat msat, capacity_msat; - struct amount_sat capacity_sats; - gossmap_chan_get_capacity(gossmap, chan, &capacity_sats); + + capacity_msat = gossmap_chan_get_capacity(gossmap, chan); gossmap_chan_get_update_details(gossmap, chan, dir, NULL, NULL, NULL, NULL, NULL, NULL, &msat); /* Special value for the common case of "max_htlc == capacity" */ - if (amount_msat_eq_sat(msat, capacity_sats)) { + if (amount_msat_eq(msat, capacity_msat)) { return 0; } /* Other common case: "max_htlc == 99% capacity" */ - if (amount_sat_to_msat(&capacity_msat, capacity_sats) - && amount_msat_scale(&capacity_msat, capacity_msat, 0.99) + if (amount_msat_scale(&capacity_msat, capacity_msat, 0.99) && amount_msat_eq(msat, capacity_msat)) { return 1; } @@ -675,9 +674,9 @@ int main(int argc, char *argv[]) /* := {capacity_count} {capacity_count}*{capacity} */ u64 *vals = tal_arr(chans, u64, channel_count); for (size_t i = 0; i < channel_count; i++) { - struct amount_sat sats; - gossmap_chan_get_capacity(gossmap, chans[i], &sats); - vals[i] = sats.satoshis; /* Raw: compression format */ + struct amount_msat cap; + cap = gossmap_chan_get_capacity(gossmap, chans[i]); + vals[i] = cap.millisatoshis / 1000; /* Raw: compression format */ } write_template_and_values(outf, vals, "capacities"); diff --git a/doc/Makefile b/doc/Makefile index fd59c4182646..b7b7648edcab 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -6,10 +6,14 @@ doc-wrongdir: GENERATE_MARKDOWN := doc/lightning-addgossip.7 \ doc/lightning-addpsbtoutput.7 \ + doc/lightning-askrene-create-layer.7 \ + doc/lightning-askrene-remove-layer.7 \ doc/lightning-askrene-create-channel.7 \ + doc/lightning-askrene-update-channel.7 \ doc/lightning-askrene-disable-node.7 \ doc/lightning-askrene-inform-channel.7 \ doc/lightning-askrene-listlayers.7 \ + doc/lightning-askrene-listreservations.7 \ doc/lightning-askrene-reserve.7 \ doc/lightning-askrene-unreserve.7 \ doc/lightning-autoclean-once.7 \ diff --git a/doc/index.rst b/doc/index.rst index 5515229db141..edee2e109cdd 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -15,11 +15,15 @@ Core Lightning Documentation lightning-addgossip lightning-addpsbtoutput lightning-askrene-create-channel + lightning-askrene-create-layer lightning-askrene-disable-node lightning-askrene-inform-channel lightning-askrene-listlayers + lightning-askrene-listreservations + lightning-askrene-remove-layer lightning-askrene-reserve lightning-askrene-unreserve + lightning-askrene-update-channel lightning-autoclean-once lightning-autoclean-status lightning-batching diff --git a/doc/schemas/lightning-askrene-create-channel.json b/doc/schemas/lightning-askrene-create-channel.json index ff43fdf7eb99..f1acd0c8299b 100644 --- a/doc/schemas/lightning-askrene-create-channel.json +++ b/doc/schemas/lightning-askrene-create-channel.json @@ -7,7 +7,7 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-create-channel** RPC command tells askrene to populate one direction of a channel in the given layer. If the channel already exists, it will be overridden. If the layer does not exist, it will be created." + "The **askrene-create-channel** RPC command tells askrene create a channel in the given layer. To actually populate the channel use *askrene-update-channel* in each direction." ], "request": { "required": [ @@ -15,12 +15,7 @@ "source", "destination", "short_channel_id", - "capacity_msat", - "htlc_min", - "htlc_max", - "base_fee", - "proportional_fee", - "delay" + "capacity_msat" ], "properties": { "layer": { @@ -50,37 +45,8 @@ "capacity_msat": { "type": "msat", "description": [ - "The capacity (onchain size) of the channel." - ] - }, - "htlc_min": { - "type": "msat", - "description": [ - "The minimum value allowed in this direction." - ] - }, - "htlc_max": { - "type": "msat", - "description": [ - "The maximum value allowed in this direction." - ] - }, - "base_fee": { - "type": "msat", - "description": [ - "The base fee to apply to use the channel in this direction." - ] - }, - "proportional_fee": { - "type": "u32", - "description": [ - "The proportional fee (in parts per million) to apply to use the channel in this direction." - ] - }, - "delay": { - "type": "u16", - "description": [ - "The CLTV delay required for this direction." + "The capacity (onchain size) of the channel.", + "NOTE: this is in millisatoshis!" ] } } @@ -92,6 +58,7 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-disable-node(7)", + "lightning-askrene-update-channel(7)", "lightning-askrene-inform-channel(7)", "lightning-askrene-listlayers(7)", "lightning-askrene-age(7)" diff --git a/doc/schemas/lightning-askrene-create-layer.json b/doc/schemas/lightning-askrene-create-layer.json new file mode 100644 index 000000000000..566a2a1e947a --- /dev/null +++ b/doc/schemas/lightning-askrene-create-layer.json @@ -0,0 +1,201 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-create-layer", + "title": "Command to create a new layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-create-layer** RPC command tells askrene to create a new, empty layer. This layer can then be populated with `askrene-create-channel` and `askrene-inform-channel`, and be used in `getroutes`." + ], + "request": { + "required": [ + "layer" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to create." + ] + } + } + }, + "response": { + "required": [ + "layers" + ], + "properties": { + "layers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "layer", + "disabled_nodes", + "created_channels", + "channel_updates", + "constraints" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer." + ] + }, + "disabled_nodes": { + "type": "array", + "items": { + "type": "pubkey", + "description": [ + "The id of the disabled node." + ] + } + }, + "disabled_channels": { + "type": "array", + "items": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction which is disabled." + ] + } + }, + "created_channels": { + "type": "array", + "items": { + "type": "object", + "required": [ + "source", + "destination", + "short_channel_id", + "capacity_msat" + ], + "properties": { + "source": { + "type": "pubkey", + "description": [ + "The source node id for the channel." + ] + }, + "destination": { + "type": "pubkey", + "description": [ + "The destination node id for the channel." + ] + }, + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id for the channel." + ] + }, + "capacity_msat": { + "type": "msat", + "description": [ + "The capacity (onchain size) of the channel." + ] + } + } + } + }, + "channel_updates": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { + "htlc_minimum_msat": { + "type": "msat", + "description": [ + "The minimum value allowed in this direction." + ] + }, + "htlc_maximum_msat": { + "type": "msat", + "description": [ + "The maximum value allowed in this direction." + ] + }, + "fee_base_msat": { + "type": "msat", + "description": [ + "The base fee to apply to use the channel in this direction." + ] + }, + "fee_proportional_millionths": { + "type": "u32", + "description": [ + "The proportional fee (in parts per million) to apply to use the channel in this direction." + ] + }, + "delay": { + "type": "u16", + "description": [ + "The CLTV delay required for this direction." + ] + } + } + } + }, + "constraints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id", + "direction" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id." + ] + }, + "direction": { + "type": "u32", + "description": [ + "The direction." + ] + }, + "maximum_msat": { + "type": "msat", + "description": [ + "The maximum value which this channel could pass. This or *minimum_msat* will be present, but not both." + ] + }, + "minimum_msat": { + "type": "msat", + "description": [ + "The minimum value which this channel could pass. This or *minimum_msat* will be present, but not both." + ] + } + } + } + } + } + } + } + } + }, + "see_also": [ + "lightning-askrene-remove-layer(7)", + "lightning-getroutes(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-askrene-inform-channel.json b/doc/schemas/lightning-askrene-inform-channel.json index 8ba0f90f62e3..b12de57fef3a 100644 --- a/doc/schemas/lightning-askrene-inform-channel.json +++ b/doc/schemas/lightning-askrene-inform-channel.json @@ -7,13 +7,14 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-inform-channel** RPC command tells askrene about the minimum or maximum current capacity of a given channel. It can be applied whether the curren channel exists or not. If the layer does not exist, it will be created." + "The **askrene-inform-channel** RPC command tells askrene about channels we used so it can update its capacity estimates. For most accuracy, you should remove your own reservations before calling this. It can be applied whether the current channel exists or not." ], "request": { "required": [ "layer", - "short_channel_id", - "direction" + "short_channel_id_dir", + "amount_msat", + "inform" ], "properties": { "layer": { @@ -22,74 +23,62 @@ "The name of the layer to apply this change to." ] }, - "short_channel_id": { - "type": "short_channel_id", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The short channel id to apply this change to." + "The short channel id and direction to apply this change to." ] }, - "direction": { - "type": "u32", - "description": [ - "The direction to apply this change to." - ] - }, - "minimum_msat": { + "amount_msat": { "type": "msat", "description": [ - "The minumum value which this channel could pass. This or *minimum_msat* must be specified, but not both." + "The amount we used on the channel" ] }, - "maximum_msat": { - "type": "msat", + "inform": { + "type": "string", + "enum": [ + "constrained", + "unconstrained", + "succeeded" + ], "description": [ - "The maximum value which this channel could pass. This or *minimum_msat* must be specified, but not both." + "Whether this payment passed (implying capacity of at least that amount), failed (implying maximum capacity of one msat less), or succeeded (implying capacity has been reduced in this direction)" ] } } }, "response": { "required": [ - "constraint" + "constraints" ], "properties": { - "constraint": { - "type": "object", - "required": [ - "short_channel_id", - "direction", - "timestamp" - ], - "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The *short_channel_id* specified." - ] - }, - "direction": { - "type": "u32", - "description": [ - "The *direction* specified." - ] - }, - "timestamp": { - "type": "u64", - "description": [ - "The UNIX time (seconds since 1970) this was created." - ] - }, - "maximum_msat": { - "type": "msat", - "description": [ - "The *minimum_msat* (if specified)" - ] - }, - "minimum_msat": { - "type": "msat", - "description": [ - "The *maximum_msat* (if specified)" - ] + "constraints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The short channel id and direction" + ] + }, + "maximum_msat": { + "type": "msat", + "description": [ + "The maximum value which this channel could pass." + ] + }, + "minimum_msat": { + "type": "msat", + "description": [ + "The minimum value which this channel could pass." + ] + } } } } diff --git a/doc/schemas/lightning-askrene-listlayers.json b/doc/schemas/lightning-askrene-listlayers.json index a0a5cba70511..c206f8fe60be 100644 --- a/doc/schemas/lightning-askrene-listlayers.json +++ b/doc/schemas/lightning-askrene-listlayers.json @@ -34,6 +34,7 @@ "layer", "disabled_nodes", "created_channels", + "channel_updates", "constraints" ], "properties": { @@ -52,6 +53,15 @@ ] } }, + "disabled_channels": { + "type": "array", + "items": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction which is disabled." + ] + } + }, "created_channels": { "type": "array", "items": { @@ -60,12 +70,7 @@ "source", "destination", "short_channel_id", - "capacity_msat", - "htlc_minimum_msat", - "htlc_maximum_msat", - "fee_base_msat", - "fee_proportional_millionths", - "delay" + "capacity_msat" ], "properties": { "source": { @@ -91,7 +96,18 @@ "description": [ "The capacity (onchain size) of the channel." ] - }, + } + } + } + }, + "channel_updates": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { "htlc_minimum_msat": { "type": "msat", "description": [ @@ -130,32 +146,25 @@ "items": { "type": "object", "required": [ - "short_channel_id", - "direction" + "short_channel_id_dir" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The short channel id." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The direction." + "The short channel id and direction" ] }, "maximum_msat": { "type": "msat", "description": [ - "The maximum value which this channel could pass. This or *minimum_msat* will be present, but not both." + "The maximum value which this channel could pass." ] }, "minimum_msat": { "type": "msat", "description": [ - "The minimum value which this channel could pass. This or *minimum_msat* will be present, but not both." + "The minimum value which this channel could pass." ] } } diff --git a/doc/schemas/lightning-askrene-listreservations.json b/doc/schemas/lightning-askrene-listreservations.json new file mode 100644 index 000000000000..1886d9e1c19e --- /dev/null +++ b/doc/schemas/lightning-askrene-listreservations.json @@ -0,0 +1,72 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-listreservations", + "title": "Command to display information about reservations (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-reservations** RPC command reports outstanding reservations made with `askrene-reserve`, mainly for debugging." + ], + "request": { + "required": [], + "properties": {} + }, + "response": { + "required": [ + "reservations" + ], + "properties": { + "layers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "short_channel_id_dir", + "amount_msat", + "age_in_seconds", + "command_id" + ], + "properties": { + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction that is reserved." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "The amount reserved." + ] + }, + "age_in_seconds": { + "type": "u64", + "description": [ + "The age of this reservation." + ] + }, + "command_id": { + "type": "string", + "description": [ + "The JSON id of the command used to make the reservation." + ] + } + } + } + } + } + }, + "see_also": [ + "lightning-askrene-reserve(7)", + "lightning-askrene-unreserve(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-askrene-remove-layer.json b/doc/schemas/lightning-askrene-remove-layer.json new file mode 100644 index 000000000000..541cf9529002 --- /dev/null +++ b/doc/schemas/lightning-askrene-remove-layer.json @@ -0,0 +1,39 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-remove-layer", + "title": "Command to destroy a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-remove-layer** RPC command tells askrene to forget a layer." + ], + "request": { + "required": [ + "layer" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to remove." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-askrene-create-layer(7)", + "lightning-askrene-listlayers(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-askrene-reserve.json b/doc/schemas/lightning-askrene-reserve.json index 7b91e3800998..880eff109dfd 100644 --- a/doc/schemas/lightning-askrene-reserve.json +++ b/doc/schemas/lightning-askrene-reserve.json @@ -7,7 +7,7 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-reserve** RPC command tells askrene that a path is being attempted. This allows it to take that into account when other *getroutes* calls are made. You should call **askrene-unreserve** after the attempt has completed.", + "The **askrene-reserve** RPC command tells askrene that a path is being attempted. This allows it to take that into account when other *getroutes* calls are made. You should call *askrene-unreserve* after the attempt has completed (and before calling *askrene-inform*).", "", "Note that additional properties inside the *path* elements are ignored, which is useful when used with the result of *getroutes*." ], @@ -22,21 +22,14 @@ "type": "object", "additionalProperties": true, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "amount_msat" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", - "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { @@ -57,11 +50,7 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-unreserve(7)", - "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", - "lightning-askrene-inform-channel(7)", - "lightning-askrene-listlayers(7)", - "lightning-askrene-age(7)" + "lightning-askrene-listreservations(7)" ], "author": [ "Rusty Russell <> is mainly responsible." diff --git a/doc/schemas/lightning-askrene-unreserve.json b/doc/schemas/lightning-askrene-unreserve.json index 377595a5caa5..c214e56ddc75 100644 --- a/doc/schemas/lightning-askrene-unreserve.json +++ b/doc/schemas/lightning-askrene-unreserve.json @@ -22,21 +22,14 @@ "type": "object", "additionalProperties": true, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "amount_msat" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", - "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { @@ -57,11 +50,7 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-reserve(7)", - "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", - "lightning-askrene-inform-channel(7)", - "lightning-askrene-listlayers(7)", - "lightning-askrene-age(7)" + "lightning-askrene-listreservations(7)" ], "author": [ "Rusty Russell <> is mainly responsible." diff --git a/doc/schemas/lightning-askrene-update-channel.json b/doc/schemas/lightning-askrene-update-channel.json new file mode 100644 index 000000000000..c0f82dcdee0f --- /dev/null +++ b/doc/schemas/lightning-askrene-update-channel.json @@ -0,0 +1,76 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-update-channel", + "title": "Command to manipulate channel in a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-update-channel** RPC command overrides updates for an existing channel when the layer is applied." + ], + "request": { + "required": [ + "layer", + "short_channel_id_dir" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction to apply the change to." + ] + }, + "htlc_min": { + "type": "msat", + "description": [ + "The minimum value allowed in this direction." + ] + }, + "htlc_max": { + "type": "msat", + "description": [ + "The maximum value allowed in this direction." + ] + }, + "base_fee": { + "type": "msat", + "description": [ + "The base fee to apply to use the channel in this direction." + ] + }, + "proportional_fee": { + "type": "u32", + "description": [ + "The proportional fee (in parts per million) to apply to use the channel in this direction." + ] + }, + "cltv_expiry_delta": { + "type": "u16", + "description": [ + "The CLTV delay required for this direction." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-create-channel(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-getroutes.json b/doc/schemas/lightning-getroutes.json index 5b09bda28c57..aeaeb2216b8a 100644 --- a/doc/schemas/lightning-getroutes.json +++ b/doc/schemas/lightning-getroutes.json @@ -121,23 +121,16 @@ "type": "object", "additionalProperties": false, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "next_node_id", "amount_msat", "delay" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", - "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index d514f59364cb..755a06f9d9d3 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -3167,7 +3167,7 @@ static struct command_result *param_dev_channel(struct command *cmd, const jsmntok_t *tok, struct channel **channel) { - struct peer *peer; + struct peer *peer COMPILER_WANTS_INIT("gcc version 12.3.0 -O3"); struct command_result *res; bool more_than_one; diff --git a/plugins/askrene/Makefile b/plugins/askrene/Makefile index a55136f75753..989905aee192 100644 --- a/plugins/askrene/Makefile +++ b/plugins/askrene/Makefile @@ -1,5 +1,5 @@ -PLUGIN_ASKRENE_SRC := plugins/askrene/askrene.c plugins/askrene/layer.c plugins/askrene/reserve.c plugins/askrene/mcf.c plugins/askrene/dijkstra.c plugins/askrene/flow.c plugins/askrene/refine.c -PLUGIN_ASKRENE_HEADER := plugins/askrene/askrene.h plugins/askrene/layer.h plugins/askrene/reserve.h plugins/askrene/mcf.h plugins/askrene/dijkstra.h plugins/askrene/flow.h plugins/askrene/refine.h +PLUGIN_ASKRENE_SRC := plugins/askrene/askrene.c plugins/askrene/layer.c plugins/askrene/reserve.c plugins/askrene/mcf.c plugins/askrene/dijkstra.c plugins/askrene/flow.c plugins/askrene/refine.c plugins/askrene/explain_failure.c +PLUGIN_ASKRENE_HEADER := plugins/askrene/askrene.h plugins/askrene/layer.h plugins/askrene/reserve.h plugins/askrene/mcf.h plugins/askrene/dijkstra.h plugins/askrene/flow.h plugins/askrene/refine.h plugins/askrene/explain_failure.h PLUGIN_ASKRENE_OBJS := $(PLUGIN_ASKRENE_SRC:.c=.o) $(PLUGIN_ASKRENE_OBJS): $(PLUGIN_ASKRENE_HEADER) @@ -7,4 +7,4 @@ $(PLUGIN_ASKRENE_OBJS): $(PLUGIN_ASKRENE_HEADER) ALL_C_SOURCES += $(PLUGIN_ASKRENE_SRC) ALL_C_HEADERS += $(PLUGIN_ASKRENE_HEADER) -plugins/cln-askrene: $(PLUGIN_ASKRENE_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) bitcoin/chainparams.o common/gossmap.o common/sciddir_or_pubkey.o common/gossmods_listpeerchannels.o common/fp16.o common/dijkstra.o common/bolt12.o common/bolt12_merkle.o wire/bolt12_wiregen.o wire/onion_wiregen.o +plugins/cln-askrene: $(PLUGIN_ASKRENE_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) bitcoin/chainparams.o common/gossmap.o common/sciddir_or_pubkey.o common/gossmods_listpeerchannels.o common/fp16.o common/dijkstra.o common/bolt12.o common/bolt12_merkle.o wire/bolt12_wiregen.o wire/onion_wiregen.o common/route.o diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 751b687416ef..db720f0d96ba 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -9,6 +9,7 @@ #include "config.h" #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -35,12 +37,6 @@ per_htlc_cost_key(const struct per_htlc_cost *phc) return &phc->scidd; } -static size_t hash_scidd(const struct short_channel_id_dir *scidd) -{ - /* scids cost money to generate, so simple hash works here */ - return (scidd->scid.u64 >> 32) ^ (scidd->scid.u64 << 1) ^ scidd->dir; -} - static inline bool per_htlc_cost_eq_key(const struct per_htlc_cost *phc, const struct short_channel_id_dir *scidd) { @@ -62,24 +58,35 @@ static bool have_layer(const char **layers, const char *name) return false; } -/* JSON helpers */ -static struct command_result *param_string_array(struct command *cmd, - const char *name, - const char *buffer, - const jsmntok_t *tok, - const char ***arr) +/* Valid, known layers */ +static struct command_result *param_layer_names(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + const char ***arr) { size_t i; const jsmntok_t *t; if (tok->type != JSMN_ARRAY) - return command_fail_badparam(cmd, name, buffer, tok, "should be an array"); + return command_fail_badparam(cmd, name, buffer, tok, + "should be an array"); *arr = tal_arr(cmd, const char *, tok->size); json_for_each_arr(i, t, tok) { if (t->type != JSMN_STRING) - return command_fail_badparam(cmd, name, buffer, t, "should be a string"); + return command_fail_badparam(cmd, name, buffer, t, + "should be a string"); (*arr)[i] = json_strdup(*arr, buffer, t); + + /* Must be a known layer name */ + if (streq((*arr)[i], "auto.localchans") + || streq((*arr)[i], "auto.sourcefree")) + continue; + if (!find_layer(get_askrene(cmd->plugin), (*arr)[i])) { + return command_fail_badparam(cmd, name, buffer, t, + "unknown layer"); + } } return NULL; } @@ -102,49 +109,17 @@ static struct command_result *param_known_layer(struct command *cmd, return NULL; } -static bool json_to_zero_or_one(const char *buffer, const jsmntok_t *tok, int *num) -{ - u32 v32; - if (!json_to_u32(buffer, tok, &v32)) - return false; - if (v32 != 0 && v32 != 1) - return false; - *num = v32; - return true; -} - -static struct command_result *param_zero_or_one(struct command *cmd, +static struct command_result *parse_reserve_hop(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, - int **num) -{ - *num = tal(cmd, int); - if (json_to_zero_or_one(buffer, tok, *num)) - return NULL; - - return command_fail_badparam(cmd, name, buffer, tok, - "should be 0 or 1"); -} - -struct reserve_path { - struct short_channel_id_dir *scidds; - struct amount_msat *amounts; -}; - -static struct command_result *parse_reserve_path(struct command *cmd, - const char *name, - const char *buffer, - const jsmntok_t *tok, - struct short_channel_id_dir *scidd, - struct amount_msat *amount) + struct reserve_hop *rhop) { const char *err; - err = json_scan(tmpctx, buffer, tok, "{short_channel_id:%,direction:%,amount_msat:%}", - JSON_SCAN(json_to_short_channel_id, &scidd->scid), - JSON_SCAN(json_to_zero_or_one, &scidd->dir), - JSON_SCAN(json_to_msat, amount)); + err = json_scan(tmpctx, buffer, tok, "{short_channel_id_dir:%,amount_msat:%}", + JSON_SCAN(json_to_short_channel_id_dir, &rhop->scidd), + JSON_SCAN(json_to_msat, &rhop->amount)); if (err) return command_fail_badparam(cmd, name, buffer, tok, err); return NULL; @@ -154,7 +129,7 @@ static struct command_result *param_reserve_path(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, - struct reserve_path **path) + struct reserve_hop **path) { size_t i; const jsmntok_t *t; @@ -162,15 +137,11 @@ static struct command_result *param_reserve_path(struct command *cmd, if (tok->type != JSMN_ARRAY) return command_fail_badparam(cmd, name, buffer, tok, "should be an array"); - *path = tal(cmd, struct reserve_path); - (*path)->scidds = tal_arr(cmd, struct short_channel_id_dir, tok->size); - (*path)->amounts = tal_arr(cmd, struct amount_msat, tok->size); + *path = tal_arr(cmd, struct reserve_hop, tok->size); json_for_each_arr(i, t, tok) { struct command_result *ret; - ret = parse_reserve_path(cmd, name, buffer, t, - &(*path)->scidds[i], - &(*path)->amounts[i]); + ret = parse_reserve_hop(cmd, name, buffer, t, &(*path)[i]); if (ret) return ret; } @@ -188,16 +159,12 @@ static fp16_t *get_capacities(const tal_t *ctx, for (c = gossmap_first_chan(gossmap); c; c = gossmap_next_chan(gossmap, c)) { - struct amount_sat cap; + struct amount_msat cap; - if (!gossmap_chan_get_capacity(gossmap, c, &cap)) { - plugin_log(plugin, LOG_BROKEN, - "get_capacity failed for channel?"); - cap = AMOUNT_SAT(0); - } + cap = gossmap_chan_get_capacity(gossmap, c); /* Pessimistic: round down! */ caps[gossmap_chan_idx(gossmap, c)] - = u64_to_fp16(cap.satoshis, false); /* Raw: fp16 */ + = u64_to_fp16(cap.millisatoshis/1000, false); /* Raw: fp16 */ } return caps; } @@ -214,11 +181,11 @@ static void add_free_source(struct plugin *plugin, /* We apply existing localmods, save up mods we want, then append * them: it's not safe to modify localmods while they are applied! */ const struct gossmap_node *srcnode; - struct mod { - struct short_channel_id_dir scidd; - fp16_t htlc_min, htlc_max; - bool enabled; - } *mods = tal_arr(tmpctx, struct mod, 0); + const struct amount_msat zero_base_fee = AMOUNT_MSAT(0); + const u16 zero_delay = 0; + const u32 zero_prop_fee = 0; + struct short_channel_id_dir *scidds + = tal_arr(tmpctx, struct short_channel_id_dir, 0); gossmap_apply_localmods(gossmap, localmods); @@ -226,35 +193,24 @@ static void add_free_source(struct plugin *plugin, srcnode = gossmap_find_node(gossmap, source); for (size_t i = 0; srcnode && i < srcnode->num_chans; i++) { + struct short_channel_id_dir scidd; const struct gossmap_chan *c; - const struct half_chan *h; - struct mod mod; - c = gossmap_nth_chan(gossmap, srcnode, i, &mod.scidd.dir); - h = &c->half[mod.scidd.dir]; - - mod.scidd.scid = gossmap_chan_scid(gossmap, c); - mod.htlc_min = h->htlc_min; - mod.htlc_max = h->htlc_max; - mod.enabled = h->enabled; - tal_arr_expand(&mods, mod); + c = gossmap_nth_chan(gossmap, srcnode, i, &scidd.dir); + scidd.scid = gossmap_chan_scid(gossmap, c); + tal_arr_expand(&scidds, scidd); } gossmap_remove_localmods(gossmap, localmods); - /* Now we can update localmods */ - for (size_t i = 0; i < tal_count(mods); i++) { + /* Now we can update localmods: we only change fee levels and delay */ + for (size_t i = 0; i < tal_count(scidds); i++) { if (!gossmap_local_updatechan(localmods, - mods[i].scidd.scid, - /* Keep min and max */ - /* FIXME: lossy conversion! */ - amount_msat(fp16_to_u64(mods[i].htlc_min)), - amount_msat(fp16_to_u64(mods[i].htlc_max)), - 0, 0, 0, - /* Keep enabled flag */ - mods[i].enabled, - mods[i].scidd.dir)) - plugin_err(plugin, "Could not zero fee on %s", - fmt_short_channel_id_dir(tmpctx, &mods[i].scidd)); + &scidds[i], + NULL, NULL, NULL, + &zero_base_fee, &zero_prop_fee, + &zero_delay)) + plugin_err(plugin, "Could not zero fee/delay on %s", + fmt_short_channel_id_dir(tmpctx, &scidds[i])); } } @@ -269,9 +225,10 @@ struct amount_msat get_additional_per_htlc_cost(const struct route_query *rq, return AMOUNT_MSAT(0); } + /* Returns an error message, or sets *routes */ static const char *get_routes(const tal_t *ctx, - struct plugin *plugin, + struct command *cmd, const struct node_id *source, const struct node_id *dest, struct amount_msat amount, @@ -285,7 +242,7 @@ static const char *get_routes(const tal_t *ctx, const struct additional_cost_htable *additional_costs, double *probability) { - struct askrene *askrene = get_askrene(plugin); + struct askrene *askrene = get_askrene(cmd->plugin); struct route_query *rq = tal(ctx, struct route_query); struct flow **flows; const struct gossmap_node *srcnode, *dstnode; @@ -300,7 +257,8 @@ static const char *get_routes(const tal_t *ctx, askrene->capacities = get_capacities(askrene, askrene->plugin, askrene->gossmap); } - rq->plugin = plugin; + rq->cmd = cmd; + rq->plugin = cmd->plugin; rq->gossmap = askrene->gossmap; rq->reserved = askrene->reserved; rq->layers = tal_arr(rq, const struct layer *, 0); @@ -311,16 +269,19 @@ static const char *get_routes(const tal_t *ctx, for (size_t i = 0; i < tal_count(layers); i++) { const struct layer *l = find_layer(askrene, layers[i]); if (!l) { - if (local_layer && streq(layers[i], "auto.localchans")) { - plugin_log(plugin, LOG_DBG, "Adding auto.localchans"); + if (streq(layers[i], "auto.localchans")) { + plugin_log(rq->plugin, LOG_DBG, "Adding auto.localchans"); l = local_layer; - } else + } else { + /* Handled below, after other layers */ + assert(streq(layers[i], "auto.sourcefree")); continue; + } } tal_arr_expand(&rq->layers, l); /* FIXME: Implement localmods_merge, and cache this in layer? */ - layer_add_localmods(l, rq->gossmap, false, localmods); + layer_add_localmods(l, rq->gossmap, localmods); /* Clear any entries in capacities array if we * override them (incl local channels) */ @@ -329,7 +290,7 @@ static const char *get_routes(const tal_t *ctx, /* This also looks into localmods, to zero them */ if (have_layer(layers, "auto.sourcefree")) - add_free_source(plugin, askrene->gossmap, localmods, source); + add_free_source(rq->plugin, askrene->gossmap, localmods, source); /* Clear scids with reservations, too, so we don't have to look up * all the time! */ @@ -340,13 +301,13 @@ static const char *get_routes(const tal_t *ctx, srcnode = gossmap_find_node(askrene->gossmap, source); if (!srcnode) { ret = tal_fmt(ctx, "Unknown source node %s", fmt_node_id(tmpctx, source)); - goto out; + goto fail; } dstnode = gossmap_find_node(askrene->gossmap, dest); if (!dstnode) { ret = tal_fmt(ctx, "Unknown destination node %s", fmt_node_id(tmpctx, dest)); - goto out; + goto fail; } delay_feefactor = 1.0/1000000; @@ -369,11 +330,8 @@ static const char *get_routes(const tal_t *ctx, flows = minflow(rq, rq, srcnode, dstnode, amount, mu, delay_feefactor, base_fee_penalty, prob_cost_factor); if (!flows) { - /* FIXME: disjktra here to see if there is any route, and - * diagnose problem (offline peers? Not enough capacity at - * our end? Not enough at theirs?) */ - ret = tal_fmt(ctx, "Could not find route"); - goto out; + ret = explain_failure(ctx, rq, srcnode, dstnode, amount); + goto fail; } /* Too much delay? */ @@ -390,24 +348,24 @@ static const char *get_routes(const tal_t *ctx, mu, delay_feefactor, base_fee_penalty, prob_cost_factor); if (!flows || delay_feefactor > 10) { ret = tal_fmt(ctx, "Could not find route without excessive delays"); - goto out; + goto fail; } } /* Too expensive? */ - while (amount_msat_greater(flowset_fee(plugin, flows), maxfee)) { + while (amount_msat_greater(flowset_fee(rq->plugin, flows), maxfee)) { mu += 10; flows = minflow(rq, rq, srcnode, dstnode, amount, mu, delay_feefactor, base_fee_penalty, prob_cost_factor); if (!flows || mu == 100) { ret = tal_fmt(ctx, "Could not find route without excessive cost"); - goto out; + goto fail; } } if (finalcltv + flows_worst_delay(flows) > 2016) { ret = tal_fmt(ctx, "Could not find route without excessive cost or delays"); - goto out; + goto fail; } /* The above did not take into account the extra funds to pay @@ -416,7 +374,7 @@ static const char *get_routes(const tal_t *ctx, * still possible */ ret = refine_with_fees_and_limits(ctx, rq, amount, &flows); if (ret) - goto out; + goto fail; /* Convert back into routes, with delay and other information fixed */ *routes = tal_arr(ctx, struct route *, tal_count(flows)); @@ -440,7 +398,7 @@ static const char *get_routes(const tal_t *ctx, const struct half_chan *h = flow_edge(flows[i], j); if (!amount_msat_add_fee(&msat, h->base_fee, h->proportional_fee)) - plugin_err(plugin, "Adding fee to amount"); + plugin_err(rq->plugin, "Adding fee to amount"); delay += h->delay; rh->scid = gossmap_chan_scid(rq->gossmap, flows[i]->path[j]); @@ -454,9 +412,13 @@ static const char *get_routes(const tal_t *ctx, } *probability = flowset_probability(flows, rq); - ret = NULL; + gossmap_remove_localmods(askrene->gossmap, localmods); + return NULL; -out: + /* Explicit failure path keeps the compiler (gcc version 12.3.0 -O3) from + * warning about uninitialized variables in the caller */ +fail: + assert(ret != NULL); gossmap_remove_localmods(askrene->gossmap, localmods); return ret; } @@ -468,7 +430,6 @@ void get_constraints(const struct route_query *rq, struct amount_msat *max) { struct short_channel_id_dir scidd; - const struct reserve *reserve; size_t idx = gossmap_chan_idx(rq->gossmap, chan); *min = AMOUNT_MSAT(0); @@ -484,45 +445,18 @@ void get_constraints(const struct route_query *rq, scidd.dir = dir; *max = AMOUNT_MSAT(-1ULL); - /* Look through layers for any constraints */ - for (size_t i = 0; i < tal_count(rq->layers); i++) { - const struct constraint *cmin, *cmax; - cmin = layer_find_constraint(rq->layers[i], &scidd, CONSTRAINT_MIN); - if (cmin && amount_msat_greater(cmin->limit, *min)) - *min = cmin->limit; - cmax = layer_find_constraint(rq->layers[i], &scidd, CONSTRAINT_MAX); - if (cmax && amount_msat_less(cmax->limit, *max)) - *max = cmax->limit; - } + /* Look through layers for any constraints (might be dummy + * ones, for created channels!) */ + for (size_t i = 0; i < tal_count(rq->layers); i++) + layer_apply_constraints(rq->layers[i], &scidd, min, max); /* Might be here because it's reserved, but capacity is normal. */ - if (amount_msat_eq(*max, AMOUNT_MSAT(-1ULL))) { - struct amount_sat cap; - if (gossmap_chan_get_capacity(rq->gossmap, chan, &cap)) { - /* Shouldn't happen! */ - if (!amount_sat_to_msat(max, cap)) { - plugin_log(rq->plugin, LOG_BROKEN, - "Local channel %s with capacity %s?", - fmt_short_channel_id(tmpctx, scidd.scid), - fmt_amount_sat(tmpctx, cap)); - } - } else { - /* Shouldn't happen: local channels have explicit constraints */ - plugin_log(rq->plugin, LOG_BROKEN, - "Channel %s without capacity?", - fmt_short_channel_id(tmpctx, scidd.scid)); - } - } + if (amount_msat_eq(*max, AMOUNT_MSAT(-1ULL))) + *max = gossmap_chan_get_capacity(rq->gossmap, chan); /* Finally, if any is in use, subtract that! */ - reserve = find_reserve(rq->reserved, &scidd); - if (reserve) { - /* They can definitely *try* to push too much through a channel! */ - if (!amount_msat_sub(min, *min, reserve->amount)) - *min = AMOUNT_MSAT(0); - if (!amount_msat_sub(max, *max, reserve->amount)) - *max = AMOUNT_MSAT(0); - } + reserve_sub(rq->reserved, &scidd, min); + reserve_sub(rq->reserved, &scidd, max); } struct getroutes_info { @@ -546,7 +480,7 @@ static struct command_result *do_getroutes(struct command *cmd, struct route **routes; struct json_stream *response; - err = get_routes(cmd, cmd->plugin, + err = get_routes(cmd, cmd, info->source, info->dest, *info->amount, *info->maxfee, *info->finalcltv, info->layers, localmods, info->local_layer, @@ -564,10 +498,12 @@ static struct command_result *do_getroutes(struct command *cmd, json_add_u32(response, "final_cltv", *info->finalcltv); json_array_start(response, "path"); for (size_t j = 0; j < tal_count(routes[i]->hops); j++) { + struct short_channel_id_dir scidd; const struct route_hop *r = &routes[i]->hops[j]; json_object_start(response, NULL); - json_add_short_channel_id(response, "short_channel_id", r->scid); - json_add_u32(response, "direction", r->direction); + scidd.scid = r->scid; + scidd.dir = r->direction; + json_add_short_channel_id_dir(response, "short_channel_id_dir", scidd); json_add_node_id(response, "next_node_id", &r->node_id); json_add_amount_msat(response, "amount_msat", r->amount); json_add_u32(response, "delay", r->delay); @@ -584,12 +520,13 @@ static void add_localchan(struct gossmap_localmods *mods, const struct node_id *self, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf, const jsmntok_t *chantok, @@ -599,7 +536,7 @@ static void add_localchan(struct gossmap_localmods *mods, const char *opener; const char *err; - gossmod_add_localchan(mods, self, peer, scidd, htlcmin, htlcmax, + gossmod_add_localchan(mods, self, peer, scidd, capacity_msat, htlcmin, htlcmax, spendable, fee_base, fee_proportional, cltv_delta, enabled, buf, chantok, info->local_layer); @@ -643,8 +580,7 @@ static void add_localchan(struct gossmap_localmods *mods, } /* Known capacity on local channels (ts = max) */ - layer_update_constraint(info->local_layer, scidd, CONSTRAINT_MIN, UINT64_MAX, spendable); - layer_update_constraint(info->local_layer, scidd, CONSTRAINT_MAX, UINT64_MAX, spendable); + layer_add_constraint(info->local_layer, scidd, UINT64_MAX, &spendable, &spendable); } static struct command_result * @@ -676,7 +612,7 @@ static struct command_result *json_getroutes(struct command *cmd, p_req("source", param_node_id, &info->source), p_req("destination", param_node_id, &info->dest), p_req("amount_msat", param_msat, &info->amount), - p_req("layers", param_string_array, &info->layers), + p_req("layers", param_layer_names, &info->layers), p_req("maxfee_msat", param_msat, &info->maxfee), p_req("final_cltv", param_u32, &info->finalcltv), NULL)) @@ -704,9 +640,8 @@ static struct command_result *json_askrene_reserve(struct command *cmd, const char *buffer, const jsmntok_t *params) { - struct reserve_path *path; + struct reserve_hop *path; struct json_stream *response; - size_t num; struct askrene *askrene = get_askrene(cmd->plugin); if (!param(cmd, buffer, params, @@ -714,17 +649,8 @@ static struct command_result *json_askrene_reserve(struct command *cmd, NULL)) return command_param_failed(); - num = reserves_add(askrene->reserved, path->scidds, path->amounts, - tal_count(path->scidds)); - if (num != tal_count(path->scidds)) { - const struct reserve *r = find_reserve(askrene->reserved, &path->scidds[num]); - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "Overflow reserving %zu: %s amount %s (%s reserved already)", - num, - fmt_short_channel_id_dir(tmpctx, &path->scidds[num]), - fmt_amount_msat(tmpctx, path->amounts[num]), - r ? fmt_amount_msat(tmpctx, r->amount) : "none"); - } + for (size_t i = 0; i < tal_count(path); i++) + reserve_add(askrene->reserved, &path[i], cmd->id); response = jsonrpc_stream_success(cmd); return command_finished(cmd, response); @@ -734,9 +660,8 @@ static struct command_result *json_askrene_unreserve(struct command *cmd, const char *buffer, const jsmntok_t *params) { - struct reserve_path *path; + struct reserve_hop *path; struct json_stream *response; - size_t num; struct askrene *askrene = get_askrene(cmd->plugin); if (!param(cmd, buffer, params, @@ -744,140 +669,181 @@ static struct command_result *json_askrene_unreserve(struct command *cmd, NULL)) return command_param_failed(); - num = reserves_remove(askrene->reserved, path->scidds, path->amounts, - tal_count(path->scidds)); - if (num != tal_count(path->scidds)) { - const struct reserve *r = find_reserve(askrene->reserved, &path->scidds[num]); - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "Underflow unreserving %zu: %s amount %s (%zu reserved, amount %s)", - num, - fmt_short_channel_id_dir(tmpctx, &path->scidds[num]), - fmt_amount_msat(tmpctx, path->amounts[num]), - r ? r->num_htlcs : 0, - r ? fmt_amount_msat(tmpctx, r->amount) : "none"); - } + for (size_t i = 0; i < tal_count(path); i++) { + if (!reserve_remove(askrene->reserved, &path[i])) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Unknown reservation for %s", + fmt_short_channel_id_dir(tmpctx, + &path[i].scidd)); + } + } response = jsonrpc_stream_success(cmd); return command_finished(cmd, response); } -static struct command_result *param_layername(struct command *cmd, - const char *name, - const char *buffer, - const jsmntok_t *tok, - const char **str) +static struct command_result *json_askrene_listreservations(struct command *cmd, + const char *buffer, + const jsmntok_t *params) { - *str = tal_strndup(cmd, buffer + tok->start, - tok->end - tok->start); - if (strstarts(*str, "auto.")) - return command_fail_badparam(cmd, name, buffer, tok, - "New layers cannot start with auto."); - return NULL; + struct askrene *askrene = get_askrene(cmd->plugin); + struct json_stream *response; + + if (!param(cmd, buffer, params, + NULL)) + return command_param_failed(); + + response = jsonrpc_stream_success(cmd); + json_add_reservations(response, askrene->reserved, "reservations"); + return command_finished(cmd, response); } static struct command_result *json_askrene_create_channel(struct command *cmd, const char *buffer, const jsmntok_t *params) { - const char *layername; struct layer *layer; - const struct local_channel *lc; struct node_id *src, *dst; struct short_channel_id *scid; struct amount_msat *capacity; struct json_stream *response; - struct amount_msat *htlc_min, *htlc_max, *base_fee; - u32 *proportional_fee; - u16 *delay; - struct askrene *askrene = get_askrene(cmd->plugin); if (!param_check(cmd, buffer, params, - p_req("layer", param_layername, &layername), + p_req("layer", param_known_layer, &layer), p_req("source", param_node_id, &src), p_req("destination", param_node_id, &dst), p_req("short_channel_id", param_short_channel_id, &scid), p_req("capacity_msat", param_msat, &capacity), - p_req("htlc_minimum_msat", param_msat, &htlc_min), - p_req("htlc_maximum_msat", param_msat, &htlc_max), - p_req("fee_base_msat", param_msat, &base_fee), - p_req("fee_proportional_millionths", param_u32, &proportional_fee), - p_req("delay", param_u16, &delay), NULL)) return command_param_failed(); - /* If it exists, it must match */ - layer = find_layer(askrene, layername); - if (layer) { - lc = layer_find_local_channel(layer, *scid); - if (lc && !layer_check_local_channel(lc, src, dst, *capacity)) { - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "channel already exists with different values!"); - } - } else - lc = NULL; + if (layer_find_local_channel(layer, *scid)) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "channel already exists"); + } if (command_check_only(cmd)) return command_check_done(cmd); - if (!layer) - layer = new_layer(askrene, layername); + layer_add_local_channel(layer, src, dst, *scid, *capacity); + + response = jsonrpc_stream_success(cmd); + return command_finished(cmd, response); +} - layer_update_local_channel(layer, src, dst, *scid, *capacity, - *base_fee, *proportional_fee, *delay, - *htlc_min, *htlc_max); +static struct command_result *json_askrene_update_channel(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct layer *layer; + struct short_channel_id_dir *scidd; + bool *enabled; + struct amount_msat *htlc_min, *htlc_max, *base_fee; + u32 *proportional_fee; + u16 *delay; + struct json_stream *response; + + if (!param(cmd, buffer, params, + p_req("layer", param_known_layer, &layer), + p_req("short_channel_id_dir", param_short_channel_id_dir, &scidd), + p_opt("enabled", param_bool, &enabled), + p_opt("htlc_minimum_msat", param_msat, &htlc_min), + p_opt("htlc_maximum_msat", param_msat, &htlc_max), + p_opt("fee_base_msat", param_msat, &base_fee), + p_opt("fee_proportional_millionths", param_u32, &proportional_fee), + p_opt("cltv_expiry_delta", param_u16, &delay), + NULL)) + return command_param_failed(); + + layer_add_update_channel(layer, scidd, + enabled, + htlc_min, htlc_max, + base_fee, proportional_fee, delay); response = jsonrpc_stream_success(cmd); return command_finished(cmd, response); } +enum inform { + INFORM_CONSTRAINED, + INFORM_UNCONSTRAINED, + INFORM_SUCCEEDED, +}; + +static struct command_result *param_inform(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + enum inform **inform) +{ + *inform = tal(cmd, enum inform); + if (json_tok_streq(buffer, tok, "constrained")) + **inform = INFORM_CONSTRAINED; + else if (json_tok_streq(buffer, tok, "unconstrained")) + **inform = INFORM_UNCONSTRAINED; + else if (json_tok_streq(buffer, tok, "succeeded")) + **inform = INFORM_SUCCEEDED; + else + command_fail_badparam(cmd, name, buffer, tok, + "must be constrained/unconstrained/succeeded"); + return NULL; +} + static struct command_result *json_askrene_inform_channel(struct command *cmd, const char *buffer, const jsmntok_t *params) { + struct askrene *askrene = get_askrene(cmd->plugin); struct layer *layer; - const char *layername; - struct short_channel_id *scid; - int *direction; + struct short_channel_id_dir *scidd; struct json_stream *response; - struct amount_msat *max, *min; + struct amount_msat *amount; + enum inform *inform; const struct constraint *c; - struct short_channel_id_dir scidd; - struct askrene *askrene = get_askrene(cmd->plugin); if (!param_check(cmd, buffer, params, - p_req("layer", param_layername, &layername), - p_req("short_channel_id", param_short_channel_id, &scid), - p_req("direction", param_zero_or_one, &direction), - p_opt("minimum_msat", param_msat, &min), - p_opt("maximum_msat", param_msat, &max), + p_req("layer", param_known_layer, &layer), + p_req("short_channel_id_dir", param_short_channel_id_dir, &scidd), + p_req("amount_msat", param_msat, &amount), + p_req("inform", param_inform, &inform), NULL)) return command_param_failed(); - if ((!min && !max) || (min && max)) { - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "Must specify exactly one of maximum_msat/minimum_msat"); + switch (*inform) { + case INFORM_CONSTRAINED: + /* It didn't pass, so minimal assumption is that reserve was all used + * then there we were one msat short. */ + if (!amount_msat_sub(amount, *amount, AMOUNT_MSAT(1))) + *amount = AMOUNT_MSAT(0); + if (!reserve_accumulate(askrene->reserved, scidd, amount)) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Amount overflow with reserves"); + if (command_check_only(cmd)) + return command_check_done(cmd); + c = layer_add_constraint(layer, scidd, time_now().ts.tv_sec, + NULL, amount); + goto output; + case INFORM_UNCONSTRAINED: + /* It passed, so the capacity is at least this much (minimal assumption is + * that no reserves were used) */ + if (command_check_only(cmd)) + return command_check_done(cmd); + c = layer_add_constraint(layer, scidd, time_now().ts.tv_sec, + amount, NULL); + goto output; + case INFORM_SUCCEEDED: + /* FIXME: We could do something useful here! */ + c = NULL; + goto output; } + abort(); - if (command_check_only(cmd)) - return command_check_done(cmd); - - layer = find_layer(askrene, layername); - if (!layer) - layer = new_layer(askrene, layername); - - /* Calls expect a convenient short_channel_id_dir struct */ - scidd.scid = *scid; - scidd.dir = *direction; - - if (min) { - c = layer_update_constraint(layer, &scidd, CONSTRAINT_MIN, - time_now().ts.tv_sec, *min); - } else { - c = layer_update_constraint(layer, &scidd, CONSTRAINT_MAX, - time_now().ts.tv_sec, *max); - } +output: response = jsonrpc_stream_success(cmd); - json_add_constraint(response, "constraint", c, layer); + json_array_start(response, "constraints"); + if (c) + json_add_constraint(response, NULL, c, layer); + json_array_end(response); return command_finished(cmd, response); } @@ -886,21 +852,15 @@ static struct command_result *json_askrene_disable_node(struct command *cmd, const jsmntok_t *params) { struct node_id *node; - const char *layername; struct layer *layer; struct json_stream *response; - struct askrene *askrene = get_askrene(cmd->plugin); if (!param(cmd, buffer, params, - p_req("layer", param_layername, &layername), + p_req("layer", param_known_layer, &layer), p_req("node", param_node_id, &node), NULL)) return command_param_failed(); - layer = find_layer(askrene, layername); - if (!layer) - layer = new_layer(askrene, layername); - /* We save this in the layer, because they want us to disable all the channels * to the node at *use* time (a new channel might be gossiped!). */ layer_add_disabled_node(layer, node); @@ -909,21 +869,71 @@ static struct command_result *json_askrene_disable_node(struct command *cmd, return command_finished(cmd, response); } +static struct command_result *json_askrene_create_layer(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct askrene *askrene = get_askrene(cmd->plugin); + struct layer *layer; + const char *layername; + struct json_stream *response; + + if (!param_check(cmd, buffer, params, + p_req("layer", param_string, &layername), + NULL)) + return command_param_failed(); + + if (find_layer(askrene, layername)) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Layer already exists"); + + if (strstarts(layername, "auto.")) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Cannot create auto layer"); + + if (command_check_only(cmd)) + return command_check_done(cmd); + + layer = new_layer(askrene, layername); + + response = jsonrpc_stream_success(cmd); + json_add_layers(response, askrene, "layers", layer); + return command_finished(cmd, response); +} + +static struct command_result *json_askrene_remove_layer(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct layer *layer; + struct json_stream *response; + + if (!param(cmd, buffer, params, + p_req("layer", param_known_layer, &layer), + NULL)) + return command_param_failed(); + + tal_free(layer); + + response = jsonrpc_stream_success(cmd); + return command_finished(cmd, response); +} + static struct command_result *json_askrene_listlayers(struct command *cmd, const char *buffer, const jsmntok_t *params) { struct askrene *askrene = get_askrene(cmd->plugin); - const char *layername; + struct layer *layer; struct json_stream *response; if (!param(cmd, buffer, params, - p_opt("layer", param_string, &layername), + p_opt("layer", param_known_layer, &layer), NULL)) return command_param_failed(); response = jsonrpc_stream_success(cmd); - json_add_layers(response, askrene, "layers", layername); + json_add_layers(response, askrene, "layers", layer); return command_finished(cmd, response); } @@ -955,6 +965,10 @@ static const struct plugin_command commands[] = { "getroutes", json_getroutes, }, + { + "askrene-listreservations", + json_askrene_listreservations, + }, { "askrene-reserve", json_askrene_reserve, @@ -971,10 +985,22 @@ static const struct plugin_command commands[] = { "askrene-create-channel", json_askrene_create_channel, }, + { + "askrene-update-channel", + json_askrene_update_channel, + }, { "askrene-inform-channel", json_askrene_inform_channel, }, + { + "askrene-create-layer", + json_askrene_create_layer, + }, + { + "askrene-remove-layer", + json_askrene_remove_layer, + }, { "askrene-listlayers", json_askrene_listlayers, diff --git a/plugins/askrene/askrene.h b/plugins/askrene/askrene.h index 7fb432838c8f..5ae9a4511ace 100644 --- a/plugins/askrene/askrene.h +++ b/plugins/askrene/askrene.h @@ -35,6 +35,9 @@ struct askrene { /* Information for a single route query. */ struct route_query { + /* Command pointer, mainly for command id. */ + struct command *cmd; + /* Plugin pointer, for logging mainly */ struct plugin *plugin; @@ -71,4 +74,11 @@ static inline struct askrene *get_askrene(struct plugin *plugin) return plugin_get_data(plugin, struct askrene); } +/* Convenience routine for hash tables */ +static inline size_t hash_scidd(const struct short_channel_id_dir *scidd) +{ + /* scids cost money to generate, so simple hash works here */ + return (scidd->scid.u64 >> 32) ^ (scidd->scid.u64 >> 16) ^ (scidd->scid.u64 << 1) ^ scidd->dir; +} + #endif /* LIGHTNING_PLUGINS_ASKRENE_ASKRENE_H */ diff --git a/plugins/askrene/explain_failure.c b/plugins/askrene/explain_failure.c new file mode 100644 index 000000000000..b8ac907d132d --- /dev/null +++ b/plugins/askrene/explain_failure.c @@ -0,0 +1,291 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#define NO_USABLE_PATHS_STRING "We could not find a usable set of paths." + +/* Dijkstra, reduced to ignore anything but connectivity */ +static bool always_true(const struct gossmap *map, + const struct gossmap_chan *c, + int dir, + struct amount_msat amount, + void *unused) +{ + return true; +} + +static u64 route_score_one(struct amount_msat fee UNUSED, + struct amount_msat risk UNUSED, + struct amount_msat total UNUSED, + int dir UNUSED, + const struct gossmap_chan *c UNUSED) +{ + return 1; +} + +/* This mirrors get_constraints() */ +static const char *why_max_constrained(const tal_t *ctx, + const struct route_query *rq, + struct short_channel_id_dir *scidd, + struct amount_msat amount) +{ + char *ret = NULL; + const char *reservations; + const struct layer *constrains = NULL; + struct amount_msat max = amount; + + /* Figure out the layer that constrains us (most) */ + for (size_t i = 0; i < tal_count(rq->layers); i++) { + struct amount_msat min = AMOUNT_MSAT(0), new_max = max; + + layer_apply_constraints(rq->layers[i], scidd, &min, &new_max); + if (!amount_msat_eq(new_max, max)) + constrains = rq->layers[i]; + max = new_max; + } + + if (constrains) { + if (!ret) + ret = tal_strdup(ctx, ""); + else + tal_append_fmt(&ret, ", "); + tal_append_fmt(&ret, "layer %s says max is %s", + layer_name(constrains), + fmt_amount_msat(tmpctx, max)); + } + + reservations = fmt_reservations(tmpctx, rq->reserved, scidd); + if (reservations) { + if (!ret) + ret = tal_strdup(ctx, ""); + else + tal_append_fmt(&ret, " and "); + tal_append_fmt(&ret, "already reserved %s", reservations); + } + + /* This seems unlikely, but don't return NULL. */ + if (!ret) + ret = tal_fmt(ctx, "is constrained"); + return ret; +} + +struct stat { + size_t num_channels; + struct amount_msat capacity; +}; + +struct node_stats { + struct stat total, gossip_known, enabled; +}; + +enum node_direction { + INTO_NODE, + OUT_OF_NODE, +}; + +static void add_stat(struct stat *stat, + struct amount_msat amount) +{ + stat->num_channels++; + if (!amount_msat_accumulate(&stat->capacity, amount)) + abort(); +} + +static void node_stats(const struct route_query *rq, + const struct gossmap_node *node, + enum node_direction node_direction, + struct node_stats *stats) +{ + memset(stats, 0, sizeof(*stats)); + for (size_t i = 0; i < node->num_chans; i++) { + int dir; + struct gossmap_chan *c; + struct amount_msat cap_msat; + + c = gossmap_nth_chan(rq->gossmap, node, i, &dir); + cap_msat = gossmap_chan_get_capacity(rq->gossmap, c); + + if (node_direction == INTO_NODE) + dir = !dir; + + add_stat(&stats->total, cap_msat); + if (gossmap_chan_set(c, dir)) + add_stat(&stats->gossip_known, cap_msat); + if (c->half[dir].enabled) + add_stat(&stats->enabled, cap_msat); + } +} + +static const char *check_capacity(const tal_t *ctx, + const struct route_query *rq, + const struct gossmap_node *node, + enum node_direction node_direction, + struct amount_msat amount, + const char *name) +{ + struct node_stats stats; + + node_stats(rq, node, node_direction, &stats); + if (amount_msat_greater(amount, stats.total.capacity)) { + return tal_fmt(ctx, + NO_USABLE_PATHS_STRING + " Total %s capacity is only %s" + " (in %zu channels).", + name, + fmt_amount_msat(tmpctx, stats.total.capacity), + stats.total.num_channels); + } + if (amount_msat_greater(amount, stats.gossip_known.capacity)) { + return tal_fmt(ctx, + NO_USABLE_PATHS_STRING + " Missing gossip for %s: only known %zu/%zu channels, leaving capacity only %s of %s.", + name, + stats.gossip_known.num_channels, + stats.total.num_channels, + fmt_amount_msat(tmpctx, stats.gossip_known.capacity), + fmt_amount_msat(tmpctx, stats.total.capacity)); + } + if (amount_msat_greater(amount, stats.enabled.capacity)) { + return tal_fmt(ctx, + NO_USABLE_PATHS_STRING + " The %s has disabled %zu of %zu channels, leaving capacity only %s of %s.", + name, + stats.total.num_channels - stats.enabled.num_channels, + stats.total.num_channels, + fmt_amount_msat(tmpctx, stats.enabled.capacity), + fmt_amount_msat(tmpctx, stats.total.capacity)); + } + return NULL; +} + +/* Return description of why scidd is disabled scidd */ +static const char *describe_disabled(const tal_t *ctx, + const struct route_query *rq, + const struct gossmap_chan *c, + const struct short_channel_id_dir *scidd) +{ + for (int i = tal_count(rq->layers) - 1; i >= 0; i--) { + struct gossmap_node *dst = gossmap_nth_node(rq->gossmap, c, !scidd->dir); + struct node_id dstid; + + gossmap_node_get_id(rq->gossmap, dst, &dstid); + if (layer_disables_node(rq->layers[i], &dstid)) + return tal_fmt(ctx, "leads to node disabled by layer %s.", + layer_name(rq->layers[i])); + else if (layer_disables_chan(rq->layers[i], scidd)) { + return tal_fmt(ctx, "marked disabled by layer %s.", + layer_name(rq->layers[i])); + } + } + + return tal_fmt(ctx, "marked disabled by gossip message."); +} + +static const char *describe_capacity(const tal_t *ctx, + const struct route_query *rq, + const struct short_channel_id_dir *scidd, + struct amount_msat amount) +{ + for (int i = tal_count(rq->layers) - 1; i >= 0; i--) { + if (layer_created(rq->layers[i], scidd->scid)) { + return tal_fmt(ctx, " (created by layer %s) isn't big enough to carry %s.", + layer_name(rq->layers[i]), + fmt_amount_msat(tmpctx, amount)); + } + } + + return tal_fmt(ctx, "isn't big enough to carry %s.", + fmt_amount_msat(tmpctx, amount)); +} + +/* We failed to find a flow at all. Why? */ +const char *explain_failure(const tal_t *ctx, + const struct route_query *rq, + const struct gossmap_node *srcnode, + const struct gossmap_node *dstnode, + struct amount_msat amount) +{ + const struct route_hop *hops; + const struct dijkstra *dij; + char *path; + const char *cap_check; + + /* Do we have enough funds? */ + cap_check = check_capacity(ctx, rq, srcnode, OUT_OF_NODE, + amount, "source"); + if (cap_check) + return cap_check; + + /* Does destination have enough capacity? */ + cap_check = check_capacity(ctx, rq, dstnode, INTO_NODE, + amount, "destination"); + if (cap_check) + return cap_check; + + /* OK, fall back to telling them why didn't shortest path + * work. This covers the "but I have a direct channel!" + * case. */ + dij = dijkstra(tmpctx, rq->gossmap, dstnode, AMOUNT_MSAT(0), 0, + always_true, route_score_one, NULL); + hops = route_from_dijkstra(tmpctx, rq->gossmap, dij, srcnode, + AMOUNT_MSAT(0), 0); + if (!hops) + return tal_fmt(ctx, "There is no connection between source and destination at all"); + + /* Description of shortest path */ + path = tal_strdup(tmpctx, ""); + for (size_t i = 0; i < tal_count(hops); i++) { + tal_append_fmt(&path, "%s%s", + i > 0 ? "->" : "", + fmt_short_channel_id(tmpctx, hops[i].scid)); + } + + /* Now walk through this: is it disabled? Insuff capacity? */ + for (size_t i = 0; i < tal_count(hops); i++) { + const char *explanation; + struct short_channel_id_dir scidd; + struct gossmap_chan *c; + struct amount_msat cap_msat, min, max, htlc_max; + + scidd.scid = hops[i].scid; + scidd.dir = hops[i].direction; + c = gossmap_find_chan(rq->gossmap, &scidd.scid); + cap_msat = gossmap_chan_get_capacity(rq->gossmap, c); + get_constraints(rq, c, scidd.dir, &min, &max); + htlc_max = amount_msat(fp16_to_u64(c->half[scidd.dir].htlc_max)); + + if (!gossmap_chan_set(c, scidd.dir)) + explanation = "has no gossip"; + else if (!c->half[scidd.dir].enabled) + explanation = describe_disabled(tmpctx, rq, c, &scidd); + else if (amount_msat_greater(amount, cap_msat)) + explanation = describe_capacity(tmpctx, rq, &scidd, amount); + else if (amount_msat_greater(amount, max)) + explanation = why_max_constrained(tmpctx, rq, + &scidd, amount); + else if (amount_msat_greater(amount, htlc_max)) + explanation = tal_fmt(tmpctx, + "exceeds htlc_maximum_msat ~%s", + fmt_amount_msat(tmpctx, htlc_max)); + else + continue; + + return tal_fmt(ctx, + NO_USABLE_PATHS_STRING + " The shortest path is %s, but %s %s", + path, + fmt_short_channel_id_dir(tmpctx, &scidd), + explanation); + } + + return tal_fmt(ctx, + "Actually, I'm not sure why we didn't find the" + " obvious route %s: perhaps this is a bug?", + path); +} diff --git a/plugins/askrene/explain_failure.h b/plugins/askrene/explain_failure.h new file mode 100644 index 000000000000..ed470f8298dc --- /dev/null +++ b/plugins/askrene/explain_failure.h @@ -0,0 +1,16 @@ +#ifndef LIGHTNING_PLUGINS_ASKRENE_EXPLAIN_FAILURE_H +#define LIGHTNING_PLUGINS_ASKRENE_EXPLAIN_FAILURE_H +#include "config.h" +#include + +struct route_query; +struct gossmap_node; + +/* When MCF returns nothing, try to explain why */ +const char *explain_failure(const tal_t *ctx, + const struct route_query *rq, + const struct gossmap_node *srcnode, + const struct gossmap_node *dstnode, + struct amount_msat amount); + +#endif /* LIGHTNING_PLUGINS_ASKRENE_EXPLAIN_FAILURE_H */ diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index 3991716dc988..17cadc8e8983 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -14,38 +14,44 @@ struct local_channel { struct node_id n1, n2; struct short_channel_id scid; struct amount_msat capacity; +}; + +struct local_update { + struct short_channel_id_dir scidd; - struct added_channel_half { - /* Other fields only valid if this is true */ - bool enabled; - u16 delay; - u32 proportional_fee; - struct amount_msat base_fee; - struct amount_msat htlc_min, htlc_max; - } half[2]; + /* Non-null fields apply. */ + const bool *enabled; + const u16 *delay; + const u32 *proportional_fee; + const struct amount_msat *base_fee; + const struct amount_msat *htlc_min, *htlc_max; }; -static const struct constraint_key * -constraint_key(const struct constraint *c) -{ - return &c->key; -} +/* A constraint reflects something we learned about a channel */ +struct constraint { + struct short_channel_id_dir scidd; + /* Time this constraint was last updated */ + u64 timestamp; + /* Non-zero means set */ + struct amount_msat min; + /* Non-0xFFFFF.... means set */ + struct amount_msat max; +}; -static size_t hash_constraint_key(const struct constraint_key *key) +static const struct short_channel_id_dir * +constraint_scidd(const struct constraint *c) { - /* scids cost money to generate, so simple hash works here */ - return (key->scidd.scid.u64 >> 32) ^ (key->scidd.scid.u64) - ^ (key->scidd.dir << 1) ^ (key->type); + return &c->scidd; } -static inline bool constraint_eq_key(const struct constraint *c, - const struct constraint_key *key) +static inline bool constraint_eq_scidd(const struct constraint *c, + const struct short_channel_id_dir *scidd) { - return short_channel_id_dir_eq(&key->scidd, &c->key.scidd) && key->type == c->key.type; + return short_channel_id_dir_eq(scidd, &c->scidd); } -HTABLE_DEFINE_TYPE(struct constraint, constraint_key, hash_constraint_key, - constraint_eq_key, constraint_hash); +HTABLE_DEFINE_TYPE(struct constraint, constraint_scidd, hash_scidd, + constraint_eq_scidd, constraint_hash); static struct short_channel_id local_channel_scid(const struct local_channel *lc) @@ -68,6 +74,21 @@ static inline bool local_channel_eq_scid(const struct local_channel *lc, HTABLE_DEFINE_TYPE(struct local_channel, local_channel_scid, hash_scid, local_channel_eq_scid, local_channel_hash); +static const struct short_channel_id_dir * +local_update_scidd(const struct local_update *lu) +{ + return &lu->scidd; +} + +static inline bool local_update_eq_scidd(const struct local_update *lu, + const struct short_channel_id_dir *scidd) +{ + return short_channel_id_dir_eq(scidd, &lu->scidd); +} + +HTABLE_DEFINE_TYPE(struct local_update, local_update_scidd, hash_scidd, + local_update_eq_scidd, local_update_hash); + struct layer { /* Inside global list of layers */ struct list_node list; @@ -78,6 +99,9 @@ struct layer { /* Completely made up local additions, indexed by scid */ struct local_channel_hash *local_channels; + /* Modifications to channels, indexed by scidd */ + struct local_update_hash *local_updates; + /* Additional info, indexed by scid+dir */ struct constraint_hash *constraints; @@ -92,6 +116,8 @@ struct layer *new_temp_layer(const tal_t *ctx, const char *name) l->name = tal_strdup(l, name); l->local_channels = tal(l, struct local_channel_hash); local_channel_hash_init(l->local_channels); + l->local_updates = tal(l, struct local_update_hash); + local_update_hash_init(l->local_updates); l->constraints = tal(l, struct constraint_hash); constraint_hash_init(l->constraints); l->disabled_nodes = tal_arr(l, struct node_id, 0); @@ -99,27 +125,19 @@ struct layer *new_temp_layer(const tal_t *ctx, const char *name) return l; } +static void destroy_layer(struct layer *l, struct askrene *askrene) +{ + list_del_from(&askrene->layers, &l->list); +} + struct layer *new_layer(struct askrene *askrene, const char *name) { struct layer *l = new_temp_layer(askrene, name); list_add(&askrene->layers, &l->list); + tal_add_destructor2(l, destroy_layer, askrene); return l; } -/* Swap if necessary to make into BOLT-7 order. Return direction. */ -static int canonicalize_node_order(const struct node_id **n1, - const struct node_id **n2) -{ - const struct node_id *tmp; - - if (node_id_cmp(*n1, *n2) < 0) - return 0; - tmp = *n2; - *n2 = *n1; - *n1 = tmp; - return 1; -} - struct layer *find_layer(struct askrene *askrene, const char *name) { struct layer *l; @@ -142,63 +160,77 @@ static struct local_channel *new_local_channel(struct layer *layer, struct amount_msat capacity) { struct local_channel *lc = tal(layer, struct local_channel); - lc->n1 = *n1; - lc->n2 = *n2; + + /* Swap if necessary to make into BOLT-7 order. */ + if (node_id_cmp(n1, n2) < 0) { + lc->n1 = *n1; + lc->n2 = *n2; + } else { + lc->n1 = *n2; + lc->n2 = *n1; + } lc->scid = scid; lc->capacity = capacity; - for (size_t i = 0; i < ARRAY_SIZE(lc->half); i++) - lc->half[i].enabled = false; - local_channel_hash_add(layer->local_channels, lc); return lc; } -bool layer_check_local_channel(const struct local_channel *lc, - const struct node_id *n1, - const struct node_id *n2, - struct amount_msat capacity) +void layer_add_local_channel(struct layer *layer, + const struct node_id *src, + const struct node_id *dst, + struct short_channel_id scid, + struct amount_msat capacity) { - canonicalize_node_order(&n1, &n2); - return node_id_eq(&lc->n1, n1) - && node_id_eq(&lc->n2, n2) - && amount_msat_eq(lc->capacity, capacity); + assert(!local_channel_hash_get(layer->local_channels, scid)); + new_local_channel(layer, src, dst, scid, capacity); } -/* Update a local channel to a layer: fails if you try to change capacity or nodes! */ -void layer_update_local_channel(struct layer *layer, - const struct node_id *src, - const struct node_id *dst, - struct short_channel_id scid, - struct amount_msat capacity, - struct amount_msat base_fee, - u32 proportional_fee, - u16 delay, - struct amount_msat htlc_min, - struct amount_msat htlc_max) +void layer_add_update_channel(struct layer *layer, + const struct short_channel_id_dir *scidd, + const bool *enabled, + const struct amount_msat *htlc_min, + const struct amount_msat *htlc_max, + const struct amount_msat *base_fee, + const u32 *proportional_fee, + const u16 *delay) { - struct local_channel *lc = local_channel_hash_get(layer->local_channels, scid); - int dir = canonicalize_node_order(&src, &dst); - struct short_channel_id_dir scidd; - - if (lc) { - assert(layer_check_local_channel(lc, src, dst, capacity)); - } else { - lc = new_local_channel(layer, src, dst, scid, capacity); + struct local_update *lu; + + lu = local_update_hash_get(layer->local_updates, scidd); + if (!lu) { + lu = tal(layer, struct local_update); + lu->scidd = *scidd; + lu->enabled = NULL; + lu->delay = NULL; + lu->proportional_fee = NULL; + lu->base_fee = lu->htlc_min = lu->htlc_max = NULL; + local_update_hash_add(layer->local_updates, lu); + } + if (enabled) { + tal_free(lu->enabled); + lu->enabled = tal_dup(lu, bool, enabled); + } + if (htlc_min) { + tal_free(lu->htlc_min); + lu->htlc_min = tal_dup(lu, struct amount_msat, htlc_min); + } + if (htlc_max) { + tal_free(lu->htlc_max); + lu->htlc_max = tal_dup(lu, struct amount_msat, htlc_max); + } + if (base_fee) { + tal_free(lu->base_fee); + lu->base_fee = tal_dup(lu, struct amount_msat, base_fee); + } + if (proportional_fee) { + tal_free(lu->proportional_fee); + lu->proportional_fee = tal_dup(lu, u32, proportional_fee); + } + if (delay) { + tal_free(lu->delay); + lu->delay = tal_dup(lu, u16, delay); } - - lc->half[dir].enabled = true; - lc->half[dir].htlc_min = htlc_min; - lc->half[dir].htlc_max = htlc_max; - lc->half[dir].base_fee = base_fee; - lc->half[dir].proportional_fee = proportional_fee; - lc->half[dir].delay = delay; - - /* We always add an explicit constraint for local channels, to simplify - * lookups. You can tell it's a fake one by the timestamp. */ - scidd.scid = scid; - scidd.dir = dir; - layer_update_constraint(layer, &scidd, CONSTRAINT_MAX, UINT64_MAX, capacity); } struct amount_msat local_channel_capacity(const struct local_channel *lc) @@ -212,50 +244,43 @@ const struct local_channel *layer_find_local_channel(const struct layer *layer, return local_channel_hash_get(layer->local_channels, scid); } -static struct constraint *layer_find_constraint_nonconst(const struct layer *layer, - const struct short_channel_id_dir *scidd, - enum constraint_type type) +void layer_apply_constraints(const struct layer *layer, + const struct short_channel_id_dir *scidd, + struct amount_msat *min, + struct amount_msat *max) { - struct constraint_key k = { *scidd, type }; - return constraint_hash_get(layer->constraints, &k); -} + struct constraint *c; + struct constraint_hash_iter cit; -/* Public one returns const */ -const struct constraint *layer_find_constraint(const struct layer *layer, - const struct short_channel_id_dir *scidd, - enum constraint_type type) -{ - return layer_find_constraint_nonconst(layer, scidd, type); + /* We can have more than one: apply them all! */ + for (c = constraint_hash_getfirst(layer->constraints, scidd, &cit); + c; + c = constraint_hash_getnext(layer->constraints, scidd, &cit)) { + *min = amount_msat_max(*min, c->min); + *max = amount_msat_min(*max, c->max); + } } -const struct constraint *layer_update_constraint(struct layer *layer, - const struct short_channel_id_dir *scidd, - enum constraint_type type, - u64 timestamp, - struct amount_msat limit) +const struct constraint *layer_add_constraint(struct layer *layer, + const struct short_channel_id_dir *scidd, + u64 timestamp, + const struct amount_msat *min, + const struct amount_msat *max) { - struct constraint *c = layer_find_constraint_nonconst(layer, scidd, type); - if (!c) { - c = tal(layer, struct constraint); - c->key.scidd = *scidd; - c->key.type = type; - c->limit = limit; - constraint_hash_add(layer->constraints, c); - } else { - switch (type) { - case CONSTRAINT_MIN: - /* Increase minimum? */ - if (amount_msat_greater(limit, c->limit)) - c->limit = limit; - break; - case CONSTRAINT_MAX: - /* Decrease maximum? */ - if (amount_msat_less(limit, c->limit)) - c->limit = limit; - break; - } - } + struct constraint *c = tal(layer, struct constraint); + c->scidd = *scidd; + + if (min) + c->min = *min; + else + c->min = AMOUNT_MSAT(0); + if (max) + c->max = *max; + else + c->max = AMOUNT_MSAT(UINT64_MAX); c->timestamp = timestamp; + + constraint_hash_add(layer->constraints, c); return c; } @@ -269,7 +294,7 @@ void layer_clear_overridden_capacities(const struct layer *layer, for (con = constraint_hash_first(layer->constraints, &conit); con; con = constraint_hash_next(layer->constraints, &conit)) { - struct gossmap_chan *c = gossmap_find_chan(gossmap, &con->key.scidd.scid); + struct gossmap_chan *c = gossmap_find_chan(gossmap, &con->scidd.scid); size_t idx; if (!c) continue; @@ -304,11 +329,12 @@ void layer_add_disabled_node(struct layer *layer, const struct node_id *node) void layer_add_localmods(const struct layer *layer, const struct gossmap *gossmap, - bool zero_cost, struct gossmap_localmods *localmods) { const struct local_channel *lc; struct local_channel_hash_iter lcit; + const struct local_update *lu; + struct local_update_hash_iter luit; /* First, disable all channels into blocked nodes (local updates * can add new ones)! */ @@ -319,73 +345,79 @@ void layer_add_localmods(const struct layer *layer, if (!node) continue; for (size_t n = 0; n < node->num_chans; n++) { - struct short_channel_id scid; + struct short_channel_id_dir scidd; struct gossmap_chan *c; - int dir; - c = gossmap_nth_chan(gossmap, node, n, &dir); - scid = gossmap_chan_scid(gossmap, c); + bool enabled = false; + c = gossmap_nth_chan(gossmap, node, n, &scidd.dir); + scidd.scid = gossmap_chan_scid(gossmap, c); /* Disabled zero-capacity on incoming */ gossmap_local_updatechan(localmods, - scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(0), - 0, - 0, - 0, - false, - !dir); + &scidd, + &enabled, + NULL, NULL, NULL, NULL, NULL); } } + /* Now create new channels */ for (lc = local_channel_hash_first(layer->local_channels, &lcit); lc; lc = local_channel_hash_next(layer->local_channels, &lcit)) { gossmap_local_addchan(localmods, - &lc->n1, &lc->n2, lc->scid, NULL); - for (size_t i = 0; i < ARRAY_SIZE(lc->half); i++) { - u64 base, propfee, delay; - if (!lc->half[i].enabled) - continue; - if (zero_cost) { - base = propfee = delay = 0; - } else { - base = lc->half[i].base_fee.millisatoshis; /* Raw: gossmap */ - propfee = lc->half[i].proportional_fee; - delay = lc->half[i].delay; - } - gossmap_local_updatechan(localmods, lc->scid, - lc->half[i].htlc_min, - lc->half[i].htlc_max, - base, propfee, delay, - true, - i); - } + &lc->n1, &lc->n2, lc->scid, lc->capacity, + NULL); + } + + /* Now update channels */ + /* Now modify channels, if they exist */ + for (lu = local_update_hash_first(layer->local_updates, &luit); + lu; + lu = local_update_hash_next(layer->local_updates, &luit)) { + gossmap_local_updatechan(localmods, &lu->scidd, + lu->enabled, + lu->htlc_min, + lu->htlc_max, + lu->base_fee, + lu->proportional_fee, + lu->delay); } } static void json_add_local_channel(struct json_stream *response, const char *fieldname, - const struct local_channel *lc, - int dir) + const struct local_channel *lc) { json_object_start(response, fieldname); - - if (dir == 0) { - json_add_node_id(response, "source", &lc->n1); - json_add_node_id(response, "destination", &lc->n2); - } else { - json_add_node_id(response, "source", &lc->n2); - json_add_node_id(response, "destination", &lc->n1); - } + json_add_node_id(response, "source", &lc->n1); + json_add_node_id(response, "destination", &lc->n2); json_add_short_channel_id(response, "short_channel_id", lc->scid); json_add_amount_msat(response, "capacity_msat", lc->capacity); - json_add_amount_msat(response, "htlc_minimum_msat", lc->half[dir].htlc_min); - json_add_amount_msat(response, "htlc_maximum_msat", lc->half[dir].htlc_max); - json_add_amount_msat(response, "fee_base_msat", lc->half[dir].base_fee); - json_add_u32(response, "fee_proportional_millionths", lc->half[dir].proportional_fee); - json_add_u32(response, "delay", lc->half[dir].delay); + json_object_end(response); +} +static void json_add_local_update(struct json_stream *response, + const char *fieldname, + const struct local_update *lu) +{ + json_object_start(response, fieldname); + json_add_short_channel_id_dir(response, "short_channel_id_dir", + lu->scidd); + if (lu->enabled) + json_add_bool(response, "enabled", *lu->enabled); + if (lu->htlc_min) + json_add_amount_msat(response, + "htlc_minimum_msat", *lu->htlc_min); + if (lu->htlc_max) + json_add_amount_msat(response, + "htlc_maximum_msat", *lu->htlc_max); + if (lu->base_fee) + json_add_amount_msat(response, "fee_base_msat", *lu->base_fee); + if (lu->proportional_fee) + json_add_u32(response, + "fee_proportional_millionths", + *lu->proportional_fee); + if (lu->delay) + json_add_u32(response, "cltv_expiry_delta", *lu->delay); json_object_end(response); } @@ -397,17 +429,12 @@ void json_add_constraint(struct json_stream *js, json_object_start(js, fieldname); if (layer) json_add_string(js, "layer", layer->name); - json_add_short_channel_id(js, "short_channel_id", c->key.scidd.scid); - json_add_u32(js, "direction", c->key.scidd.dir); + json_add_short_channel_id_dir(js, "short_channel_id_dir", c->scidd); json_add_u64(js, "timestamp", c->timestamp); - switch (c->key.type) { - case CONSTRAINT_MIN: - json_add_amount_msat(js, "minimum_msat", c->limit); - break; - case CONSTRAINT_MAX: - json_add_amount_msat(js, "maximum_msat", c->limit); - break; - } + if (!amount_msat_is_zero(c->min)) + json_add_amount_msat(js, "minimum_msat", c->min); + if (!amount_msat_eq(c->max, AMOUNT_MSAT(UINT64_MAX))) + json_add_amount_msat(js, "maximum_msat", c->max); json_object_end(js); } @@ -417,6 +444,8 @@ static void json_add_layer(struct json_stream *js, { struct local_channel_hash_iter lcit; const struct local_channel *lc; + const struct local_update *lu; + struct local_update_hash_iter luit; struct constraint_hash_iter conit; const struct constraint *c; @@ -430,10 +459,14 @@ static void json_add_layer(struct json_stream *js, for (lc = local_channel_hash_first(layer->local_channels, &lcit); lc; lc = local_channel_hash_next(layer->local_channels, &lcit)) { - for (size_t i = 0; i < ARRAY_SIZE(lc->half); i++) { - if (lc->half[i].enabled) - json_add_local_channel(js, NULL, lc, i); - } + json_add_local_channel(js, NULL, lc); + } + json_array_end(js); + json_array_start(js, "channel_updates"); + for (lu = local_update_hash_first(layer->local_updates, &luit); + lu; + lu = local_update_hash_next(layer->local_updates, &luit)) { + json_add_local_update(js, NULL, lu); } json_array_end(js); json_array_start(js, "constraints"); @@ -452,19 +485,44 @@ static void json_add_layer(struct json_stream *js, void json_add_layers(struct json_stream *js, struct askrene *askrene, const char *fieldname, - const char *layername) + const struct layer *layer) { struct layer *l; json_array_start(js, fieldname); list_for_each(&askrene->layers, l, list) { - if (layername && !streq(l->name, layername)) + if (layer && l != layer) continue; json_add_layer(js, NULL, l); } json_array_end(js); } +bool layer_created(const struct layer *layer, struct short_channel_id scid) +{ + return local_channel_hash_get(layer->local_channels, scid); +} + +bool layer_disables_chan(const struct layer *layer, + const struct short_channel_id_dir *scidd) +{ + const struct local_update *lu; + + lu = local_update_hash_get(layer->local_updates, scidd); + + return (lu && lu->enabled && *lu->enabled == false); +} + +bool layer_disables_node(const struct layer *layer, + const struct node_id *node) +{ + for (size_t i = 0; i < tal_count(layer->disabled_nodes); i++) { + if (node_id_eq(&layer->disabled_nodes[i], node)) + return true; + } + return false; +} + void layer_memleak_mark(struct askrene *askrene, struct htable *memtable) { struct layer *l; diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h index 7acea220c072..0bac5744a62b 100644 --- a/plugins/askrene/layer.h +++ b/plugins/askrene/layer.h @@ -17,24 +17,6 @@ struct askrene; struct layer; struct json_stream; -enum constraint_type { - CONSTRAINT_MIN, - CONSTRAINT_MAX, -}; - -struct constraint_key { - struct short_channel_id_dir scidd; - enum constraint_type type; -}; - -/* A constraint reflects something we learned about a channel */ -struct constraint { - struct constraint_key key; - /* Time this constraint was last updated */ - u64 timestamp; - struct amount_msat limit; -}; - /* Look up a layer by name. */ struct layer *find_layer(struct askrene *askrene, const char *name); @@ -60,17 +42,22 @@ bool layer_check_local_channel(const struct local_channel *lc, const struct node_id *n2, struct amount_msat capacity); -/* Update a local channel to a layer: fails if you try to change capacity or nodes! */ -void layer_update_local_channel(struct layer *layer, - const struct node_id *src, - const struct node_id *dst, - struct short_channel_id scid, - struct amount_msat capacity, - struct amount_msat base_fee, - u32 proportional_fee, - u16 delay, - struct amount_msat htlc_min, - struct amount_msat htlc_max); +/* Add a local channel to a layer! */ +void layer_add_local_channel(struct layer *layer, + const struct node_id *src, + const struct node_id *dst, + struct short_channel_id scid, + struct amount_msat capacity); + +/* Update details on a channel (could be in this layer, or another) */ +void layer_add_update_channel(struct layer *layer, + const struct short_channel_id_dir *scidd, + const bool *enabled, + const struct amount_msat *htlc_min, + const struct amount_msat *htlc_max, + const struct amount_msat *base_fee, + const u32 *proportional_fee, + const u16 *delay); /* If any capacities of channels are limited, unset the corresponding element in * the capacities[] array */ @@ -78,22 +65,23 @@ void layer_clear_overridden_capacities(const struct layer *layer, const struct gossmap *gossmap, fp16_t *capacities); -/* Find a constraint in a layer. */ -const struct constraint *layer_find_constraint(const struct layer *layer, - const struct short_channel_id_dir *scidd, - enum constraint_type type); - -/* Add/update a constraint on a layer. */ -const struct constraint *layer_update_constraint(struct layer *layer, - const struct short_channel_id_dir *scidd, - enum constraint_type type, - u64 timestamp, - struct amount_msat limit); - -/* Add local channels from this layer. zero_cost means set fees and delay to 0. */ +/* Apply constraints from a layer (reduce min, increase max). */ +void layer_apply_constraints(const struct layer *layer, + const struct short_channel_id_dir *scidd, + struct amount_msat *min, + struct amount_msat *max) + NO_NULL_ARGS; + +/* Add one or more constraints on a layer. */ +const struct constraint *layer_add_constraint(struct layer *layer, + const struct short_channel_id_dir *scidd, + u64 timestamp, + const struct amount_msat *min, + const struct amount_msat *max); + +/* Add local channels from this layer. */ void layer_add_localmods(const struct layer *layer, const struct gossmap *gossmap, - bool zero_cost, struct gossmap_localmods *localmods); /* Remove constraints older then cutoff: returns num removed. */ @@ -102,11 +90,11 @@ size_t layer_trim_constraints(struct layer *layer, u64 cutoff); /* Add a disabled node to a layer. */ void layer_add_disabled_node(struct layer *layer, const struct node_id *node); -/* Print out a json object per layer, or all if layer is NULL */ +/* Print out a json object for this layer, or all if layer is NULL */ void json_add_layers(struct json_stream *js, struct askrene *askrene, const char *fieldname, - const char *layername); + const struct layer *layer); /* Print a single constraint */ void json_add_constraint(struct json_stream *js, @@ -114,6 +102,15 @@ void json_add_constraint(struct json_stream *js, const struct constraint *c, const struct layer *layer); +/* For explain_failure: did this layer create this scid? */ +bool layer_created(const struct layer *layer, struct short_channel_id scid); + +/* For explain_failure: did this layer disable this channel? */ +bool layer_disables_chan(const struct layer *layer, const struct short_channel_id_dir *scidd); + +/* For explain_failure: did this layer disable this node? */ +bool layer_disables_node(const struct layer *layer, const struct node_id *node); + /* Scan for memleaks */ void layer_memleak_mark(struct askrene *askrene, struct htable *memtable); #endif /* LIGHTNING_PLUGINS_ASKRENE_LAYER_H */ diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c index 8a72c6a229fe..aaa914b03afe 100644 --- a/plugins/askrene/mcf.c +++ b/plugins/askrene/mcf.c @@ -626,7 +626,7 @@ init_linear_network(const tal_t *ctx, const struct pay_parameters *params) const struct gossmap_chan *c = gossmap_nth_chan(gossmap, node, j, &half); - if (!gossmap_chan_set(c, half)) + if (!gossmap_chan_set(c, half) || !c->half[half].enabled) continue; const u32 chan_id = gossmap_chan_idx(gossmap, c); diff --git a/plugins/askrene/refine.c b/plugins/askrene/refine.c index e78eebf7e571..18596281594f 100644 --- a/plugins/askrene/refine.c +++ b/plugins/askrene/refine.c @@ -9,10 +9,6 @@ /* We (ab)use the reservation system to place temporary reservations * on channels while we are refining each flow. This has the effect * of making flows aware of each other. */ -struct reservations { - struct short_channel_id_dir *scidds; - struct amount_msat *amounts; -}; /* Get the scidd for the i'th hop in flow */ static void get_scidd(const struct gossmap *gossmap, @@ -24,50 +20,38 @@ static void get_scidd(const struct gossmap *gossmap, scidd->dir = flow->dirs[i]; } -static void destroy_reservations(struct reservations *r, struct askrene *askrene) +static void destroy_reservations(struct reserve_hop *rhops, struct askrene *askrene) { - assert(tal_count(r->scidds) == tal_count(r->amounts)); - if (reserves_remove(askrene->reserved, - r->scidds, r->amounts, - tal_count(r->scidds)) != tal_count(r->scidds)) { - plugin_err(askrene->plugin, "Failed to remove reservations?"); - } + for (size_t i = 0; i < tal_count(rhops); i++) + reserve_remove(askrene->reserved, &rhops[i]); } -static struct reservations *new_reservations(const tal_t *ctx, - struct route_query *rq) +static struct reserve_hop *new_reservations(const tal_t *ctx, + const struct route_query *rq) { - struct reservations *r = tal(ctx, struct reservations); - r->scidds = tal_arr(r, struct short_channel_id_dir, 0); - r->amounts = tal_arr(r, struct amount_msat, 0); + struct reserve_hop *rhops = tal_arr(ctx, struct reserve_hop, 0); /* Unreserve on free */ - tal_add_destructor2(r, destroy_reservations, get_askrene(rq->plugin)); - return r; + tal_add_destructor2(rhops, destroy_reservations, get_askrene(rq->plugin)); + return rhops; } /* Add reservation: we (ab)use this to temporarily avoid over-usage as * we refine. */ -static void add_reservation(struct reservations *r, +static void add_reservation(struct reserve_hop **reservations, struct route_query *rq, const struct flow *flow, size_t i, struct amount_msat amt) { - struct short_channel_id_dir scidd; + struct reserve_hop rhop; struct askrene *askrene = get_askrene(rq->plugin); size_t idx; - get_scidd(rq->gossmap, flow, i, &scidd); + get_scidd(rq->gossmap, flow, i, &rhop.scidd); + rhop.amount = amt; - /* This should not happen, but simply don't reserve if it does */ - if (!reserves_add(askrene->reserved, &scidd, &amt, 1)) { - plugin_log(rq->plugin, LOG_BROKEN, - "Failed to reserve %s in %s", - fmt_amount_msat(tmpctx, amt), - fmt_short_channel_id_dir(tmpctx, &scidd)); - return; - } + reserve_add(askrene->reserved, &rhop, rq->cmd->id); /* Set capacities entry to 0 so it get_constraints() looks in reserve. */ idx = gossmap_chan_idx(rq->gossmap, flow->path[i]); @@ -75,8 +59,7 @@ static void add_reservation(struct reservations *r, rq->capacities[idx] = 0; /* Record so destructor will unreserve */ - tal_arr_expand(&r->scidds, scidd); - tal_arr_expand(&r->amounts, amt); + tal_arr_expand(reservations, rhop); } /* We have a basic set of flows, but we need to add fees, and take into @@ -93,7 +76,7 @@ static void add_reservation(struct reservations *r, static const char *constrain_flow(const tal_t *ctx, struct route_query *rq, struct flow *flow, - struct reservations *reservations) + struct reserve_hop **reservations) { struct amount_msat msat; int decreased = -1; @@ -187,7 +170,7 @@ static const char *constrain_flow(const tal_t *ctx, /* Flow is now delivering `extra` additional msat, so modify reservations */ static void add_to_flow(struct flow *flow, struct route_query *rq, - struct reservations *reservations, + struct reserve_hop **reservations, struct amount_msat extra) { struct amount_msat orig, updated; @@ -299,7 +282,7 @@ refine_with_fees_and_limits(const tal_t *ctx, struct amount_msat deliver, struct flow ***flows) { - struct reservations *reservations = new_reservations(NULL, rq); + struct reserve_hop *reservations = new_reservations(NULL, rq); struct amount_msat more_to_deliver; const char *flow_constraint_error = NULL; const char *ret; @@ -317,7 +300,7 @@ refine_with_fees_and_limits(const tal_t *ctx, fmt_amount_msat(tmpctx, max)); } - flow_constraint_error = constrain_flow(tmpctx, rq, flow, reservations); + flow_constraint_error = constrain_flow(tmpctx, rq, flow, &reservations); if (!flow_constraint_error) { i++; continue; @@ -398,7 +381,7 @@ refine_with_fees_and_limits(const tal_t *ctx, } /* Make this flow deliver +extra, and modify reservations */ - add_to_flow(f, rq, reservations, extra); + add_to_flow(f, rq, &reservations, extra); /* Should not happen, since extra comes from div... */ if (!amount_msat_sub(&more_to_deliver, more_to_deliver, extra)) diff --git a/plugins/askrene/reserve.c b/plugins/askrene/reserve.c index fb822143c068..82c9096d3c27 100644 --- a/plugins/askrene/reserve.c +++ b/plugins/askrene/reserve.c @@ -1,28 +1,34 @@ #include "config.h" #include #include +#include #include +#include #include #include #include +/* Note! We can have multiple of these! */ +struct reserve { + /* What */ + struct reserve_hop rhop; + /* When */ + struct timemono timestamp; + /* ID of command which reserved it */ + const char *cmd_id; +}; + /* Hash table for reservations */ static const struct short_channel_id_dir * reserve_scidd(const struct reserve *r) { - return &r->scidd; -} - -static size_t hash_scidd(const struct short_channel_id_dir *scidd) -{ - /* scids cost money to generate, so simple hash works here */ - return (scidd->scid.u64 >> 32) ^ (scidd->scid.u64 >> 16) ^ (scidd->scid.u64 << 1) ^ scidd->dir; + return &r->rhop.scidd; } static bool reserve_eq_scidd(const struct reserve *r, const struct short_channel_id_dir *scidd) { - return short_channel_id_dir_eq(scidd, &r->scidd); + return short_channel_id_dir_eq(scidd, &r->rhop.scidd); } HTABLE_DEFINE_TYPE(struct reserve, reserve_scidd, hash_scidd, @@ -35,110 +41,140 @@ struct reserve_htable *new_reserve_htable(const tal_t *ctx) return reserved; } -/* Find a reservation for this scidd (if any!) */ -const struct reserve *find_reserve(const struct reserve_htable *reserved, - const struct short_channel_id_dir *scidd) -{ - return reserve_htable_get(reserved, scidd); -} - -/* Create a new (empty) reservation */ -static struct reserve *new_reserve(struct reserve_htable *reserved, - const struct short_channel_id_dir *scidd) +void reserve_add(struct reserve_htable *reserved, + const struct reserve_hop *rhop, + const char *cmd_id TAKES) { struct reserve *r = tal(reserved, struct reserve); - - r->num_htlcs = 0; - r->amount = AMOUNT_MSAT(0); - r->scidd = *scidd; + r->rhop = *rhop; + r->timestamp = time_mono(); + r->cmd_id = tal_strdup(r, cmd_id); reserve_htable_add(reserved, r); - return r; } -static void del_reserve(struct reserve_htable *reserved, struct reserve *r) +bool reserve_remove(struct reserve_htable *reserved, + const struct reserve_hop *rhop) { - assert(r->num_htlcs == 0); + struct reserve *r; + struct reserve_htable_iter rit; - reserve_htable_del(reserved, r); - tal_free(r); -} + /* Note! This may remove the "wrong" one, but since they're only + * differentiated for debugging, that's OK */ + for (r = reserve_htable_getfirst(reserved, &rhop->scidd, &rit); + r; + r = reserve_htable_getnext(reserved, &rhop->scidd, &rit)) { + if (!amount_msat_eq(r->rhop.amount, rhop->amount)) + continue; -/* Add to existing reservation (false if would overflow). */ -static bool add(struct reserve *r, struct amount_msat amount) -{ - if (!amount_msat_accumulate(&r->amount, amount)) - return false; - r->num_htlcs++; - return true; + reserve_htable_del(reserved, r); + tal_free(r); + return true; + } + return false; } -static bool remove(struct reserve *r, struct amount_msat amount) +void reserves_clear_capacities(struct reserve_htable *reserved, + const struct gossmap *gossmap, + fp16_t *capacities) { - if (r->num_htlcs == 0) - return false; - if (!amount_msat_sub(&r->amount, r->amount, amount)) - return false; - r->num_htlcs--; - return true; + struct reserve *r; + struct reserve_htable_iter rit; + + for (r = reserve_htable_first(reserved, &rit); + r; + r = reserve_htable_next(reserved, &rit)) { + struct gossmap_chan *c = gossmap_find_chan(gossmap, &r->rhop.scidd.scid); + size_t idx; + if (!c) + continue; + idx = gossmap_chan_idx(gossmap, c); + if (idx < tal_count(capacities)) + capacities[idx] = 0; + } } -/* Atomically add to reserves, or fail. - * Returns offset of failure, or num on success */ -size_t reserves_add(struct reserve_htable *reserved, - const struct short_channel_id_dir *scidds, - const struct amount_msat *amounts, - size_t num) +void reserve_sub(const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd, + struct amount_msat *amount) { - for (size_t i = 0; i < num; i++) { - struct reserve *r = reserve_htable_get(reserved, &scidds[i]); - if (!r) - r = new_reserve(reserved, &scidds[i]); - if (!add(r, amounts[i])) { - reserves_remove(reserved, scidds, amounts, i); - return i; - } + struct reserve *r; + struct reserve_htable_iter rit; + + for (r = reserve_htable_getfirst(reserved, scidd, &rit); + r; + r = reserve_htable_getnext(reserved, scidd, &rit)) { + if (!amount_msat_sub(amount, *amount, r->rhop.amount)) + *amount = AMOUNT_MSAT(0); } - return num; } -/* Atomically remove from reserves, to fail. - * Returns offset of failure or tal_count(scidds) */ -size_t reserves_remove(struct reserve_htable *reserved, - const struct short_channel_id_dir *scidds, - const struct amount_msat *amounts, - size_t num) +bool reserve_accumulate(const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd, + struct amount_msat *amount) { - for (size_t i = 0; i < num; i++) { - struct reserve *r = reserve_htable_get(reserved, &scidds[i]); - if (!r || !remove(r, amounts[i])) { - reserves_add(reserved, scidds, amounts, i); - return i; - } - if (r->num_htlcs == 0) - del_reserve(reserved, r); + struct reserve *r; + struct reserve_htable_iter rit; + + for (r = reserve_htable_getfirst(reserved, scidd, &rit); + r; + r = reserve_htable_getnext(reserved, scidd, &rit)) { + if (!amount_msat_add(amount, *amount, r->rhop.amount)) + return false; } - return num; + return true; } -void reserves_clear_capacities(struct reserve_htable *reserved, - const struct gossmap *gossmap, - fp16_t *capacities) +void json_add_reservations(struct json_stream *js, + const struct reserve_htable *reserved, + const char *fieldname) { struct reserve *r; struct reserve_htable_iter rit; + json_array_start(js, fieldname); for (r = reserve_htable_first(reserved, &rit); r; r = reserve_htable_next(reserved, &rit)) { - struct gossmap_chan *c = gossmap_find_chan(gossmap, &r->scidd.scid); - size_t idx; - if (!c) - continue; - idx = gossmap_chan_idx(gossmap, c); - if (idx < tal_count(capacities)) - capacities[idx] = 0; + json_object_start(js, NULL); + json_add_short_channel_id_dir(js, + "short_channel_id_dir", + r->rhop.scidd); + json_add_amount_msat(js, + "amount_msat", + r->rhop.amount); + json_add_u64(js, "age_in_seconds", + timemono_between(time_mono(), r->timestamp).ts.tv_sec); + json_add_string(js, "command_id", r->cmd_id); + json_object_end(js); + } + json_array_end(js); +} + +const char *fmt_reservations(const tal_t *ctx, + const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd) +{ + struct reserve *r; + struct reserve_htable_iter rit; + char *ret = NULL; + + for (r = reserve_htable_getfirst(reserved, scidd, &rit); + r; + r = reserve_htable_getnext(reserved, scidd, &rit)) { + u64 seconds; + if (!ret) + ret = tal_strdup(ctx, ""); + else + tal_append_fmt(&ret, ", "); + tal_append_fmt(&ret, "%s by command %s", + fmt_amount_msat(tmpctx, r->rhop.amount), r->cmd_id); + seconds = timemono_between(time_mono(), r->timestamp).ts.tv_sec; + /* Add a note if it's old */ + if (seconds > 0) + tal_append_fmt(&ret, " (%"PRIu64" seconds ago)", seconds); } + return ret; } void reserve_memleak_mark(struct askrene *askrene, struct htable *memtable) diff --git a/plugins/askrene/reserve.h b/plugins/askrene/reserve.h index 7da617229c56..1c9e71eaaf60 100644 --- a/plugins/askrene/reserve.h +++ b/plugins/askrene/reserve.h @@ -8,39 +8,48 @@ #include #include -/* We reserve a path being used. This records how many and how much */ -struct reserve { - size_t num_htlcs; - struct short_channel_id_dir scidd; - struct amount_msat amount; -}; - /* Initialize hash table for reservations */ struct reserve_htable *new_reserve_htable(const tal_t *ctx); -/* Find a reservation for this scidd (if any!) */ -const struct reserve *find_reserve(const struct reserve_htable *reserved, - const struct short_channel_id_dir *scidd); +struct reserve_hop { + struct short_channel_id_dir scidd; + struct amount_msat amount; +}; -/* Atomically add to reserves, or fail. - * Returns offset of failure, or num on success */ -size_t reserves_add(struct reserve_htable *reserved, - const struct short_channel_id_dir *scidds, - const struct amount_msat *amounts, - size_t num); +/* Add a reservation */ +void reserve_add(struct reserve_htable *reserved, + const struct reserve_hop *rhop, + const char *cmd_id TAKES); -/* Atomically remove from reserves, to fail. - * Returns offset of failure or tal_count(scidds) */ -size_t reserves_remove(struct reserve_htable *reserved, - const struct short_channel_id_dir *scidds, - const struct amount_msat *amounts, - size_t num); +/* Try to remove a reservation, if it exists. */ +bool reserve_remove(struct reserve_htable *reserved, + const struct reserve_hop *rhop); /* Clear capacities array where we have reserves */ void reserves_clear_capacities(struct reserve_htable *reserved, const struct gossmap *gossmap, fp16_t *capacities); +/* Subtract any reserves for scidd from this amount */ +void reserve_sub(const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd, + struct amount_msat *amount); + +/* Add any reserves for scidd to this amount */ +bool reserve_accumulate(const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd, + struct amount_msat *amount); + +/* To explain why we couldn't route */ +const char *fmt_reservations(const tal_t *ctx, + const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd); + +/* Print out a json object for all reservations */ +void json_add_reservations(struct json_stream *js, + const struct reserve_htable *reserved, + const char *fieldname); + /* Scan for memleaks */ void reserve_memleak_mark(struct askrene *askrene, struct htable *memtable); #endif /* LIGHTNING_PLUGINS_ASKRENE_RESERVE_H */ diff --git a/plugins/establish_onion_path.c b/plugins/establish_onion_path.c index 9906a55ea28f..5aa723fb3093 100644 --- a/plugins/establish_onion_path.c +++ b/plugins/establish_onion_path.c @@ -95,7 +95,8 @@ gossmods_from_listpeers(const tal_t *ctx, struct node_id peer_id; const char *err; u8 *features = NULL; - struct short_channel_id fake_scid; + struct short_channel_id_dir fake_scidd; + bool enabled = true; err = json_scan(tmpctx, buf, peer, "{connected:%," @@ -112,13 +113,13 @@ gossmods_from_listpeers(const tal_t *ctx, continue; /* Add a fake channel */ - fake_scid.u64 = i; + fake_scidd.scid.u64 = i; + fake_scidd.dir = node_id_idx(self, &peer_id); - gossmap_local_addchan(mods, self, &peer_id, fake_scid, NULL); - gossmap_local_updatechan(mods, fake_scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(0), - 0, 0, 0, true, 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; } diff --git a/plugins/libplugin-pay.c b/plugins/libplugin-pay.c index dd953661a019..572fcc9e3751 100644 --- a/plugins/libplugin-pay.c +++ b/plugins/libplugin-pay.c @@ -856,15 +856,12 @@ static double capacity_bias(const struct gossmap *map, int dir, struct amount_msat amount) { - struct amount_sat capacity; u64 amtmsat = amount.millisatoshis; /* Raw: lengthy math */ double capmsat; /* Can fail in theory if gossmap changed underneath. */ - if (!gossmap_chan_get_capacity(map, c, &capacity)) - return 0; + capmsat = (double)gossmap_chan_get_capacity(map, c).millisatoshis; /* Raw: log */ - capmsat = (double)capacity.satoshis * 1000; /* Raw: lengthy math */ return -log((capmsat + 1 - amtmsat) / (capmsat + 1)); } diff --git a/plugins/renepay/chan_extra.c b/plugins/renepay/chan_extra.c index 57183227ea3b..931463ca53d2 100644 --- a/plugins/renepay/chan_extra.c +++ b/plugins/renepay/chan_extra.c @@ -559,13 +559,8 @@ get_chan_extra_half_by_chan_verify(const struct gossmap *gossmap, struct chan_extra_half *h = get_chan_extra_half_by_scid(chan_extra_map, &scidd); if (!h) { - struct amount_sat cap; - struct amount_msat cap_msat; - - if (!gossmap_chan_get_capacity(gossmap, chan, &cap) || - !amount_sat_to_msat(&cap_msat, cap)) { - return NULL; - } + struct amount_msat cap_msat + = gossmap_chan_get_capacity(gossmap, chan); h = &new_chan_extra(chan_extra_map, scidd.scid, cap_msat) ->half[scidd.dir]; } diff --git a/plugins/renepay/mods.c b/plugins/renepay/mods.c index 49b420f66e9b..3d410cbce0d5 100644 --- a/plugins/renepay/mods.c +++ b/plugins/renepay/mods.c @@ -384,12 +384,13 @@ static void gossmod_cb(struct gossmap_localmods *mods, const struct node_id *self, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf, const jsmntok_t *chantok, @@ -408,14 +409,12 @@ static void gossmod_cb(struct gossmap_localmods *mods, } /* FIXME: features? */ - gossmap_local_addchan(mods, self, peer, scidd->scid, NULL); - - gossmap_local_updatechan(mods, scidd->scid, min, max, - fee_base.millisatoshis, /* Raw: gossmap */ - fee_proportional, - cltv_delta, - enabled, - scidd->dir); + gossmap_local_addchan(mods, self, peer, scidd->scid, capacity_msat, + NULL); + gossmap_local_updatechan(mods, scidd, + &enabled, + &min, &max, + &fee_base, &fee_proportional, &cltv_delta); /* Is it disabled? */ if (!enabled) @@ -516,13 +515,19 @@ static void add_hintchan(struct payment *payment, const struct node_id *src, assert(payment); assert(payment->local_gossmods); - int dir = node_id_idx(src, dst); - const char *errmsg; const struct chan_extra *ce = uncertainty_find_channel(pay_plugin->uncertainty, scid); if (!ce) { + struct short_channel_id_dir scidd; + /* We assume any HTLC is allowed */ + struct amount_msat htlc_min = AMOUNT_MSAT(0), htlc_max = MAX_CAPACITY; + struct amount_msat fee_base = amount_msat(fee_base_msat); + bool enabled = true; + scidd.scid = scid; + scidd.dir = node_id_idx(src, dst); + /* This channel is not public, we don't know his capacity One possible solution is set the capacity to MAX_CAP and the state to [0,MAX_CAP]. Alternatively we could @@ -540,13 +545,12 @@ static void add_hintchan(struct payment *payment, const struct node_id *src, } /* FIXME: features? */ if (!gossmap_local_addchan(payment->local_gossmods, src, dst, - scid, NULL) || + scid, MAX_CAPACITY, NULL) || !gossmap_local_updatechan( - payment->local_gossmods, scid, - /* We assume any HTLC is allowed */ - AMOUNT_MSAT(0), MAX_CAPACITY, fee_base_msat, - fee_proportional_millionths, cltv_expiry_delta, true, - dir)) { + payment->local_gossmods, &scidd, + &enabled, &htlc_min, &htlc_max, + &fee_base, &fee_proportional_millionths, + &cltv_expiry_delta)) { errmsg = tal_fmt( tmpctx, "Failed to update scid=%s in the local_gossmods.", diff --git a/plugins/renepay/test/common.h b/plugins/renepay/test/common.h index 3368bdb49e54..75d3b1f3aeb8 100644 --- a/plugins/renepay/test/common.h +++ b/plugins/renepay/test/common.h @@ -49,8 +49,7 @@ static void add_connection(int store_fd, struct amount_msat max, u32 base_fee, s32 proportional_fee, u32 delay, - struct amount_sat capacity, - bool add_capacity) + struct amount_sat capacity) { secp256k1_ecdsa_signature dummy_sig; struct secret not_a_secret; @@ -79,10 +78,8 @@ static void add_connection(int store_fd, &dummy_key, &dummy_key); write_to_store(store_fd, msg); - if (add_capacity) { - msg = towire_gossip_store_channel_amount(tmpctx, capacity); - write_to_store(store_fd, msg); - } + msg = towire_gossip_store_channel_amount(tmpctx, capacity); + write_to_store(store_fd, msg); u8 flags = node_id_idx(from, to); diff --git a/plugins/renepay/test/run-bottleneck.c b/plugins/renepay/test/run-bottleneck.c index 510ddebde5e7..2173ebd54392 100644 --- a/plugins/renepay/test/run-bottleneck.c +++ b/plugins/renepay/test/run-bottleneck.c @@ -113,32 +113,28 @@ int main(int argc, char *argv[]) AMOUNT_MSAT(0), AMOUNT_MSAT(60 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(60 * 1000), - true); + AMOUNT_SAT(60 * 1000)); assert(mk_short_channel_id(&scid, 1, 3, 0)); add_connection(fd, &nodes[0], &nodes[2], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(60 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(60 * 1000), - true); + AMOUNT_SAT(60 * 1000)); assert(mk_short_channel_id(&scid, 2, 4, 0)); add_connection(fd, &nodes[1], &nodes[3], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(mk_short_channel_id(&scid, 3, 4, 0)); add_connection(fd, &nodes[2], &nodes[3], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(mk_short_channel_id(&scid, 4, 5, 0)); add_connection(fd, &nodes[3], &nodes[4], scid, @@ -148,40 +144,35 @@ int main(int argc, char *argv[]) * through this channel. */ AMOUNT_MSAT(106 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(110 * 1000), - true); + AMOUNT_SAT(110 * 1000)); assert(mk_short_channel_id(&scid, 5, 6, 0)); add_connection(fd, &nodes[4], &nodes[5], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 100 * 1000 /* 10% */, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(mk_short_channel_id(&scid, 5, 7, 0)); add_connection(fd, &nodes[4], &nodes[6], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 100 * 1000 /* 10% */, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(mk_short_channel_id(&scid, 6, 8, 0)); add_connection(fd, &nodes[5], &nodes[7], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(mk_short_channel_id(&scid, 7, 8, 0)); add_connection(fd, &nodes[6], &nodes[7], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(gossmap_refresh(gossmap, NULL)); struct uncertainty *uncertainty = uncertainty_new(tmpctx); diff --git a/plugins/renepay/test/run-mcf-diamond.c b/plugins/renepay/test/run-mcf-diamond.c index 57e90db1dca2..2db20d6e876a 100644 --- a/plugins/renepay/test/run-mcf-diamond.c +++ b/plugins/renepay/test/run-mcf-diamond.c @@ -133,36 +133,36 @@ int main(int argc, char *argv[]) mods = gossmap_localmods_new(tmpctx); /* 1->2->4 has capacity 10k sat, 1->3->4 has capacity 5k sat (lower fee!) */ - assert(gossmap_local_addchan(mods, &l1, &l2, scid12, NULL)); - assert(gossmap_local_updatechan(mods, scid12, - /*htlc_min=*/ AMOUNT_MSAT(0), - /*htlc_max=*/ AMOUNT_MSAT(10000000), - /*base_fee=*/ 0, - /*ppm_fee =*/ 1001, - /* delay =*/ 5, - /* enabled=*/ true, - /* dir =*/ 0)); - assert(gossmap_local_addchan(mods, &l2, &l4, scid24, NULL)); - assert(gossmap_local_updatechan(mods, scid24, - AMOUNT_MSAT(0), - AMOUNT_MSAT(10000000), - 0, 1002, 5, - true, - 0)); - assert(gossmap_local_addchan(mods, &l1, &l3, scid13, NULL)); - assert(gossmap_local_updatechan(mods, scid13, - AMOUNT_MSAT(0), - AMOUNT_MSAT(5000000), - 0, 503, 5, - true, - 0)); - assert(gossmap_local_addchan(mods, &l3, &l4, scid34, NULL)); - assert(gossmap_local_updatechan(mods, scid34, - AMOUNT_MSAT(0), - AMOUNT_MSAT(5000000), - 0, 504, 5, - true, - 0)); + assert(gossmap_local_addchan(mods, &l1, &l2, scid12, AMOUNT_MSAT(10000000), NULL)); + assert(gossmap_local_setchan(mods, scid12, + /*htlc_min=*/ AMOUNT_MSAT(0), + /*htlc_max=*/ AMOUNT_MSAT(10000000), + /*base_fee=*/ AMOUNT_MSAT(0), + /*ppm_fee =*/ 1001, + /* delay =*/ 5, + /* enabled=*/ true, + /* dir =*/ 0)); + assert(gossmap_local_addchan(mods, &l2, &l4, scid24, AMOUNT_MSAT(10000000), NULL)); + assert(gossmap_local_setchan(mods, scid24, + AMOUNT_MSAT(0), + AMOUNT_MSAT(10000000), + AMOUNT_MSAT(0), 1002, 5, + true, + 0)); + assert(gossmap_local_addchan(mods, &l1, &l3, scid13, AMOUNT_MSAT(5000000), NULL)); + assert(gossmap_local_setchan(mods, scid13, + AMOUNT_MSAT(0), + AMOUNT_MSAT(5000000), + AMOUNT_MSAT(0), 503, 5, + true, + 0)); + assert(gossmap_local_addchan(mods, &l3, &l4, scid34, AMOUNT_MSAT(5000000), NULL)); + assert(gossmap_local_setchan(mods, scid34, + AMOUNT_MSAT(0), + AMOUNT_MSAT(5000000), + AMOUNT_MSAT(0), 504, 5, + true, + 0)); gossmap_apply_localmods(gossmap, mods); chan_extra_map = tal(tmpctx, struct chan_extra_map); diff --git a/plugins/renepay/test/run-mcf.c b/plugins/renepay/test/run-mcf.c index 79c291fe92c4..487304918580 100644 --- a/plugins/renepay/test/run-mcf.c +++ b/plugins/renepay/test/run-mcf.c @@ -480,13 +480,14 @@ int main(int argc, char *argv[]) assert(short_channel_id_from_str("111x1x1", 7, &scid13)); /* 400,000sat channel from 1->3, basefee 0, ppm 1000, delay 5 */ - assert(gossmap_local_addchan(mods, &l1, &l3, scid13, NULL)); - assert(gossmap_local_updatechan(mods, scid13, - AMOUNT_MSAT(0), - AMOUNT_MSAT(400000000), - 0, 1000, 5, - true, - 0)); + assert(gossmap_local_addchan(mods, &l1, &l3, scid13, + AMOUNT_MSAT(400000000), NULL)); + assert(gossmap_local_setchan(mods, scid13, + AMOUNT_MSAT(0), + AMOUNT_MSAT(400000000), + AMOUNT_MSAT(0), 1000, 5, + true, + 0)); /* Apply changes, check they work. */ gossmap_apply_localmods(gossmap, mods); @@ -494,6 +495,7 @@ int main(int argc, char *argv[]) assert(local_chan); /* The local chans have no "capacity", so set it manually. */ + /* FIXME: They do now! */ uncertainty_add_channel(uncertainty, scid13, AMOUNT_MSAT(400000000)); // flows = minflow(tmpctx, gossmap, diff --git a/plugins/renepay/test/run-missingcapacity.c b/plugins/renepay/test/run-missingcapacity.c deleted file mode 100644 index 4dc53ec4825f..000000000000 --- a/plugins/renepay/test/run-missingcapacity.c +++ /dev/null @@ -1,179 +0,0 @@ -/* Checks that uncertainty_update and get_routes can handle a gossmap where the - * capacity of some channels are missing. - * - * */ -#include "config.h" - -#include "../disabledmap.c" -#include "../errorcodes.c" -#include "../flow.c" -#include "../mcf.c" -#include "../route.c" -#include "../routebuilder.c" -#include "../uncertainty.c" -#include "common.h" - -#include -#include -#include -#include -#include -#include - -/* AUTOGENERATED MOCKS START */ -/* Generated stub for sciddir_or_pubkey_from_node_id */ -bool sciddir_or_pubkey_from_node_id(struct sciddir_or_pubkey *sciddpk UNNEEDED, - const struct node_id *node_id UNNEEDED) -{ fprintf(stderr, "sciddir_or_pubkey_from_node_id called!\n"); abort(); } -/* AUTOGENERATED MOCKS END */ - -static u8 empty_map[] = {10}; - -#define NUM_NODES 4 - -static void remove_file(char *fname) { assert(!remove(fname)); } - -int main(int argc, char *argv[]) -{ - int fd; - char *gossfile; - struct gossmap *gossmap; - struct node_id nodes[NUM_NODES]; - - common_setup(argv[0]); - chainparams = chainparams_for_network("regtest"); - - fd = tmpdir_mkstemp(tmpctx, "run-missingcapacity.XXXXXX", &gossfile); - tal_add_destructor(gossfile, remove_file); - assert(write(fd, empty_map, sizeof(empty_map)) == sizeof(empty_map)); - - gossmap = gossmap_load(tmpctx, gossfile, NULL); - assert(gossmap); - - for (size_t i = 0; i < NUM_NODES; i++) { - struct privkey tmp; - memset(&tmp, i+1, sizeof(tmp)); - node_id_from_privkey(&tmp, &nodes[i]); - } - - /* We will try a payment from 1 to 4. - * There are two possible routes 1->2->4 or 1->3->4. - * However, we will simulate that we don't have channel 3->4's capacity - * in the gossmap (see #7194). We expect that 3->4 it's simply ignored - * and only route through 1->2->4 is used. - * - * +--2--+ - * | | - * 1 4 - * | | - * +--3--+ - * - * */ - struct short_channel_id scid; - - assert(mk_short_channel_id(&scid, 1, 2, 0)); - add_connection(fd, &nodes[0], &nodes[1], scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(1000 * 1000 * 1000), - 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - /* add capacity? = */ true); - - assert(mk_short_channel_id(&scid, 2, 4, 0)); - add_connection(fd, &nodes[1], &nodes[3], scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(1000 * 1000 * 1000), - 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - /* add capacity? = */ true); - - assert(mk_short_channel_id(&scid, 1, 3, 0)); - add_connection(fd, &nodes[0], &nodes[2], scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(1000 * 1000 * 1000), - 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - /* add capacity? = */ true); - - assert(mk_short_channel_id(&scid, 3, 4, 0)); - add_connection(fd, &nodes[2], &nodes[3], scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(1000 * 1000 * 1000), - 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - /* add capacity? = */ false); - - assert(gossmap_refresh(gossmap, NULL)); - struct uncertainty *uncertainty = uncertainty_new(tmpctx); - int skipped_count = - uncertainty_update(uncertainty, gossmap); - assert(skipped_count==1); - - struct preimage preimage; - - struct amount_msat maxfee = AMOUNT_MSAT(20*1000); - struct payment_info pinfo; - pinfo.invstr = NULL; - pinfo.label = NULL; - pinfo.description = NULL; - pinfo.payment_secret = NULL; - pinfo.payment_metadata = NULL; - pinfo.routehints = NULL; - pinfo.destination = nodes[3]; - pinfo.amount = AMOUNT_MSAT(100 * 1000 * 1000); - - assert(amount_msat_add(&pinfo.maxspend, maxfee, pinfo.amount)); - pinfo.maxdelay = 100; - pinfo.final_cltv = 5; - - pinfo.start_time = time_now(); - pinfo.stop_time = timeabs_add(pinfo.start_time, time_from_sec(10000)); - - pinfo.base_fee_penalty = 1e-5; - pinfo.prob_cost_factor = 1e-5; - pinfo.delay_feefactor = 1e-6; - pinfo.min_prob_success = 0.9; - pinfo.base_prob_success = 1.0; - pinfo.use_shadow = false; - - randombytes_buf(&preimage, sizeof(preimage)); - sha256(&pinfo.payment_hash, &preimage, sizeof(preimage)); - - struct disabledmap *disabledmap = disabledmap_new(tmpctx); - - enum jsonrpc_errcode errcode; - const char *err_msg; - - u64 groupid = 1; - u64 next_partid=1; - - struct route **routes = get_routes( - /* ctx */tmpctx, - /* payment */&pinfo, - /* source */&nodes[0], - /* destination */&nodes[3], - /* gossmap */gossmap, - /* uncertainty */uncertainty, - disabledmap, - /* amount */ pinfo.amount, - /* feebudget */maxfee, - &next_partid, - groupid, - &errcode, - &err_msg); - - assert(routes); - - if (!routes) { - printf("get_route failed with error %d: %s", errcode, err_msg); - } else { - printf("get_routes: %s\n", print_routes(tmpctx, routes)); - assert(tal_count(routes) == 1); - assert(tal_count(routes[0]->hops) == 2); - assert(node_id_eq(&routes[0]->hops[0].node_id, &nodes[1])); - assert(node_id_eq(&routes[0]->hops[1].node_id, &nodes[3])); - } - - common_shutdown(); -} - diff --git a/plugins/renepay/test/run-testflow.c b/plugins/renepay/test/run-testflow.c index 5d907bae1547..a570fd9eb7d0 100644 --- a/plugins/renepay/test/run-testflow.c +++ b/plugins/renepay/test/run-testflow.c @@ -578,12 +578,12 @@ static void test_flow_to_route(void) struct chan_extra_half *h0,*h1; struct gossmap_chan *c; - struct amount_sat cap; + struct amount_msat cap; struct amount_msat sum_min1_max0,sum_min0_max1; // check the bounds channel 1--2 c = gossmap_find_chan(gossmap,&scid12); - assert(gossmap_chan_get_capacity(gossmap,c,&cap)); + cap = gossmap_chan_get_capacity(gossmap,c); h0 = get_chan_extra_half_by_chan_verify(gossmap,chan_extra_map,c,0); assert(h0); @@ -596,17 +596,17 @@ static void test_flow_to_route(void) h1->known_max = AMOUNT_MSAT(1000000000); assert(amount_msat_less_eq(h0->known_min,h0->known_max)); - assert(amount_msat_less_eq_sat(h0->known_max,cap)); + assert(amount_msat_less_eq(h0->known_max,cap)); assert(amount_msat_less_eq(h1->known_min,h1->known_max)); - assert(amount_msat_less_eq_sat(h1->known_max,cap)); + assert(amount_msat_less_eq(h1->known_max,cap)); assert(amount_msat_add(&sum_min1_max0,h1->known_min,h0->known_max)); assert(amount_msat_add(&sum_min0_max1,h0->known_min,h1->known_max)); - assert(amount_msat_eq_sat(sum_min1_max0,cap)); - assert(amount_msat_eq_sat(sum_min0_max1,cap)); + assert(amount_msat_eq(sum_min1_max0,cap)); + assert(amount_msat_eq(sum_min0_max1,cap)); // check the bounds channel 2--3 c = gossmap_find_chan(gossmap,&scid23); - assert(gossmap_chan_get_capacity(gossmap,c,&cap)); + cap = gossmap_chan_get_capacity(gossmap,c); h1 = get_chan_extra_half_by_chan_verify(gossmap,chan_extra_map,c,1); assert(h1); @@ -619,17 +619,17 @@ static void test_flow_to_route(void) h0->known_max = AMOUNT_MSAT(2000000000); assert(amount_msat_less_eq(h0->known_min,h0->known_max)); - assert(amount_msat_less_eq_sat(h0->known_max,cap)); + assert(amount_msat_less_eq(h0->known_max,cap)); assert(amount_msat_less_eq(h1->known_min,h1->known_max)); - assert(amount_msat_less_eq_sat(h1->known_max,cap)); + assert(amount_msat_less_eq(h1->known_max,cap)); assert(amount_msat_add(&sum_min1_max0,h1->known_min,h0->known_max)); assert(amount_msat_add(&sum_min0_max1,h0->known_min,h1->known_max)); - assert(amount_msat_eq_sat(sum_min1_max0,cap)); - assert(amount_msat_eq_sat(sum_min0_max1,cap)); + assert(amount_msat_eq(sum_min1_max0,cap)); + assert(amount_msat_eq(sum_min0_max1,cap)); // check the bounds channel 3--4 c = gossmap_find_chan(gossmap,&scid34); - assert(gossmap_chan_get_capacity(gossmap,c,&cap)); + cap = gossmap_chan_get_capacity(gossmap,c); h0 = get_chan_extra_half_by_chan_verify(gossmap,chan_extra_map,c,0); assert(h0); @@ -642,17 +642,17 @@ static void test_flow_to_route(void) h1->known_max = AMOUNT_MSAT(500000000); assert(amount_msat_less_eq(h0->known_min,h0->known_max)); - assert(amount_msat_less_eq_sat(h0->known_max,cap)); + assert(amount_msat_less_eq(h0->known_max,cap)); assert(amount_msat_less_eq(h1->known_min,h1->known_max)); - assert(amount_msat_less_eq_sat(h1->known_max,cap)); + assert(amount_msat_less_eq(h1->known_max,cap)); assert(amount_msat_add(&sum_min1_max0,h1->known_min,h0->known_max)); assert(amount_msat_add(&sum_min0_max1,h0->known_min,h1->known_max)); - assert(amount_msat_eq_sat(sum_min1_max0,cap)); - assert(amount_msat_eq_sat(sum_min0_max1,cap)); + assert(amount_msat_eq(sum_min1_max0,cap)); + assert(amount_msat_eq(sum_min0_max1,cap)); // check the bounds channel 4--5 c = gossmap_find_chan(gossmap,&scid45); - assert(gossmap_chan_get_capacity(gossmap,c,&cap)); + cap = gossmap_chan_get_capacity(gossmap,c); h0 = get_chan_extra_half_by_chan_verify(gossmap,chan_extra_map,c,0); assert(h0); @@ -665,13 +665,13 @@ static void test_flow_to_route(void) h1->known_max = AMOUNT_MSAT(2000000000); assert(amount_msat_less_eq(h0->known_min,h0->known_max)); - assert(amount_msat_less_eq_sat(h0->known_max,cap)); + assert(amount_msat_less_eq(h0->known_max,cap)); assert(amount_msat_less_eq(h1->known_min,h1->known_max)); - assert(amount_msat_less_eq_sat(h1->known_max,cap)); + assert(amount_msat_less_eq(h1->known_max,cap)); assert(amount_msat_add(&sum_min1_max0,h1->known_min,h0->known_max)); assert(amount_msat_add(&sum_min0_max1,h0->known_min,h1->known_max)); - assert(amount_msat_eq_sat(sum_min1_max0,cap)); - assert(amount_msat_eq_sat(sum_min0_max1,cap)); + assert(amount_msat_eq(sum_min1_max0,cap)); + assert(amount_msat_eq(sum_min0_max1,cap)); struct flow *F; struct route *route; diff --git a/plugins/renepay/uncertainty.c b/plugins/renepay/uncertainty.c index 00c50ebc1134..d151471229a0 100644 --- a/plugins/renepay/uncertainty.c +++ b/plugins/renepay/uncertainty.c @@ -104,12 +104,10 @@ int uncertainty_update(struct uncertainty *uncertainty, struct gossmap *gossmap) struct chan_extra *ce = chan_extra_map_get(chan_extra_map, scid); if (!ce) { - struct amount_sat cap; struct amount_msat cap_msat; - if (!gossmap_chan_get_capacity(gossmap, chan, &cap) || - !amount_sat_to_msat(&cap_msat, cap) || - !new_chan_extra(chan_extra_map, scid, + cap_msat = gossmap_chan_get_capacity(gossmap, chan); + if (!new_chan_extra(chan_extra_map, scid, cap_msat)) { /* If the new chan_extra cannot be created we * skip this channel. */ diff --git a/plugins/test/Makefile b/plugins/test/Makefile index 0165ee8c646c..2ecba481c65a 100644 --- a/plugins/test/Makefile +++ b/plugins/test/Makefile @@ -19,7 +19,8 @@ plugins/test/run-route-overlong: \ common/fp16.o \ common/gossmap.o \ common/node_id.o \ - common/route.o + common/route.o \ + gossipd/gossip_store_wiregen.o plugins/test/run-route-calc: \ common/fp16.o \ diff --git a/plugins/test/run-route-calc.c b/plugins/test/run-route-calc.c index da73c25a9a8b..7a8d23a18fbf 100644 --- a/plugins/test/run-route-calc.c +++ b/plugins/test/run-route-calc.c @@ -42,12 +42,13 @@ void gossmod_add_localchan(struct gossmap_localmods *mods UNNEEDED, const struct node_id *self UNNEEDED, const struct node_id *peer UNNEEDED, const struct short_channel_id_dir *scidd UNNEEDED, + struct amount_msat capacity_msat UNNEEDED, struct amount_msat htlcmin UNNEEDED, struct amount_msat htlcmax UNNEEDED, struct amount_msat spendable UNNEEDED, struct amount_msat fee_base UNNEEDED, u32 fee_proportional UNNEEDED, - u32 cltv_delta UNNEEDED, + u16 cltv_delta UNNEEDED, bool enabled UNNEEDED, const char *buf UNUSED UNNEEDED, const jsmntok_t *chantok UNUSED UNNEEDED, @@ -63,12 +64,13 @@ struct gossmap_localmods *gossmods_from_listpeerchannels_(const tal_t *ctx UNNEE const struct node_id *self_ UNNEEDED, const struct node_id *peer UNNEEDED, const struct short_channel_id_dir *scidd UNNEEDED, + struct amount_msat capacity_msat UNNEEDED, struct amount_msat htlcmin UNNEEDED, struct amount_msat htlcmax UNNEEDED, struct amount_msat spendable UNNEEDED, struct amount_msat fee_base UNNEEDED, u32 fee_proportional UNNEEDED, - u32 cltv_delta UNNEEDED, + u16 cltv_delta UNNEEDED, bool enabled UNNEEDED, const char *buf_ UNNEEDED, const jsmntok_t *chantok UNNEEDED, diff --git a/plugins/test/run-route-overlong.c b/plugins/test/run-route-overlong.c index 4a7d94b3f7d2..9655104413ed 100644 --- a/plugins/test/run-route-overlong.c +++ b/plugins/test/run-route-overlong.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -39,12 +40,13 @@ void gossmod_add_localchan(struct gossmap_localmods *mods UNNEEDED, const struct node_id *self UNNEEDED, const struct node_id *peer UNNEEDED, const struct short_channel_id_dir *scidd UNNEEDED, + struct amount_msat capacity_msat UNNEEDED, struct amount_msat htlcmin UNNEEDED, struct amount_msat htlcmax UNNEEDED, struct amount_msat spendable UNNEEDED, struct amount_msat fee_base UNNEEDED, u32 fee_proportional UNNEEDED, - u32 cltv_delta UNNEEDED, + u16 cltv_delta UNNEEDED, bool enabled UNNEEDED, const char *buf UNUSED UNNEEDED, const jsmntok_t *chantok UNUSED UNNEEDED, @@ -60,12 +62,13 @@ struct gossmap_localmods *gossmods_from_listpeerchannels_(const tal_t *ctx UNNEE const struct node_id *self_ UNNEEDED, const struct node_id *peer UNNEEDED, const struct short_channel_id_dir *scidd UNNEEDED, + struct amount_msat capacity_msat UNNEEDED, struct amount_msat htlcmin UNNEEDED, struct amount_msat htlcmax UNNEEDED, struct amount_msat spendable UNNEEDED, struct amount_msat fee_base UNNEEDED, u32 fee_proportional UNNEEDED, - u32 cltv_delta UNNEEDED, + u16 cltv_delta UNNEEDED, bool enabled UNNEEDED, const char *buf_ UNNEEDED, const jsmntok_t *chantok UNNEEDED, @@ -381,6 +384,8 @@ static void add_connection(int store_fd, ids[0], ids[1], &dummy_key, &dummy_key); write_to_store(store_fd, msg); + msg = towire_gossip_store_channel_amount(tmpctx, AMOUNT_SAT(10000)); + write_to_store(store_fd, msg); update_connection(store_fd, from, to, scid, min, max, base_fee, proportional_fee, diff --git a/plugins/topology.c b/plugins/topology.c index 972853e3826b..5cc00aa894b2 100644 --- a/plugins/topology.c +++ b/plugins/topology.c @@ -224,7 +224,7 @@ static void json_add_halfchan(struct json_stream *response, struct short_channel_id scid; struct node_id node_id[2]; const u8 *chanfeatures; - struct amount_sat capacity; + struct amount_msat capacity_msat; bool local_disable; /* These are channel (not per-direction) properties */ @@ -234,9 +234,7 @@ static void json_add_halfchan(struct json_stream *response, gossmap_node_get_id(gossmap, gossmap_nth_node(gossmap, c, i), &node_id[i]); - /* This can theoretically happen on partial write races. */ - if (!gossmap_chan_get_capacity(gossmap, c, &capacity)) - capacity = AMOUNT_SAT(0); + capacity_msat = gossmap_chan_get_capacity(gossmap, c); /* Deprecated: local channels are not "active" unless peer is connected. */ if (connected && node_id_eq(&node_id[0], &local_id)) @@ -287,7 +285,7 @@ static void json_add_halfchan(struct json_stream *response, &htlc_maximum_msat); } - json_add_amount_sat_msat(response, "amount_msat", capacity); + json_add_amount_msat(response, "amount_msat", capacity_msat); json_add_num(response, "message_flags", message_flags); json_add_num(response, "channel_flags", channel_flags); @@ -362,12 +360,13 @@ static void gossmod_add_unknown_localchan(struct gossmap_localmods *mods, const struct node_id *self, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat min, struct amount_msat max, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf UNUSED, const jsmntok_t *chantok UNUSED, @@ -376,7 +375,8 @@ static void gossmod_add_unknown_localchan(struct gossmap_localmods *mods, if (gossmap_find_chan(gossmap, &scidd->scid)) return; - gossmod_add_localchan(mods, self, peer, scidd, min, max, spendable, + gossmod_add_localchan(mods, self, peer, scidd, capacity_msat, + min, max, spendable, fee_base, fee_proportional, cltv_delta, enabled, buf, chantok, gossmap); } diff --git a/tests/test_askrene.py b/tests/test_askrene.py index a87ae3c6ec53..5a7e7abe7c2a 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -8,51 +8,193 @@ import time +def direction(src, dst): + """BOLT 7 direction: 0 means from lesser encoded id""" + if src < dst: + return 0 + return 1 + + +def test_reserve(node_factory): + """Test reserving channels""" + l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) + + assert l1.rpc.askrene_listreservations() == {'reservations': []} + scid12 = first_scid(l1, l2) + scid23 = first_scid(l2, l3) + scid12dir = f"{scid12}/{direction(l1.info['id'], l2.info['id'])}" + scid23dir = f"{scid23}/{direction(l2.info['id'], l3.info['id'])}" + + initial_prob = l1.rpc.getroutes(source=l1.info['id'], + destination=l3.info['id'], + amount_msat=1000000, + layers=[], + maxfee_msat=100000, + final_cltv=0)['probability_ppm'] + + # Reserve 1000 sats on path. This should reduce probability! + l1.rpc.askrene_reserve(path=[{'short_channel_id_dir': scid12dir, + 'amount_msat': 1000_000}, + {'short_channel_id_dir': scid23dir, + 'amount_msat': 1000_001}]) + listres = l1.rpc.askrene_listreservations()['reservations'] + if listres[0]['short_channel_id_dir'] == scid12dir: + assert listres[0]['amount_msat'] == 1000_000 + assert listres[1]['short_channel_id_dir'] == scid23dir + assert listres[1]['amount_msat'] == 1000_001 + else: + assert listres[0]['short_channel_id_dir'] == scid23dir + assert listres[0]['amount_msat'] == 1000_001 + assert listres[1]['short_channel_id_dir'] == scid12dir + assert listres[1]['amount_msat'] == 1000_000 + assert len(listres) == 2 + + assert l1.rpc.getroutes(source=l1.info['id'], + destination=l3.info['id'], + amount_msat=1000000, + layers=[], + maxfee_msat=100000, + final_cltv=0)['probability_ppm'] < initial_prob + + # Now reserve so much there's nothing left. + l1.rpc.askrene_reserve(path=[{'short_channel_id_dir': scid12dir, + 'amount_msat': 1000_000_000_000}, + {'short_channel_id_dir': scid23dir, + 'amount_msat': 1000_000_000_000}]) + + # Keep it consistent: the below will mention a time if >= 1 seconds old, + # which might happen without the sleep on slow machines. + time.sleep(2) + + # Reservations can be in either order. + with pytest.raises(RpcError, match=rf'We could not find a usable set of paths. The shortest path is {scid12}->{scid23}, but {scid12dir} already reserved 10000000*msat by command ".*" \([0-9]* seconds ago\), 10000000*msat by command ".*" \([0-9]* seconds ago\)'): + l1.rpc.getroutes(source=l1.info['id'], + destination=l3.info['id'], + amount_msat=1000000, + layers=[], + maxfee_msat=100000, + final_cltv=0)['probability_ppm'] + + # Can't remove wrong amounts: that's user error + with pytest.raises(RpcError, match="Unknown reservation"): + l1.rpc.askrene_unreserve(path=[{'short_channel_id_dir': scid12dir, + 'amount_msat': 1000_001}, + {'short_channel_id_dir': scid23dir, + 'amount_msat': 1000_000}]) + + # Remove, it's all ok. + l1.rpc.askrene_unreserve(path=[{'short_channel_id_dir': scid12dir, + 'amount_msat': 1000_000}, + {'short_channel_id_dir': scid23dir, + 'amount_msat': 1000_001}]) + l1.rpc.askrene_unreserve(path=[{'short_channel_id_dir': scid12dir, + 'amount_msat': 1000_000_000_000}, + {'short_channel_id_dir': scid23dir, + 'amount_msat': 1000_000_000_000}]) + assert l1.rpc.askrene_listreservations() == {'reservations': []} + assert l1.rpc.getroutes(source=l1.info['id'], + destination=l3.info['id'], + amount_msat=1000000, + layers=[], + maxfee_msat=100000, + final_cltv=0)['probability_ppm'] == initial_prob + + # Reserving in reverse makes no difference! + scid12rev = f"{first_scid(l1, l2)}/{direction(l2.info['id'], l1.info['id'])}" + scid23rev = f"{first_scid(l2, l3)}/{direction(l3.info['id'], l2.info['id'])}" + l1.rpc.askrene_reserve(path=[{'short_channel_id_dir': scid12rev, + 'amount_msat': 1000_000_000_000}, + {'short_channel_id_dir': scid23rev, + 'amount_msat': 1000_000_000_000}]) + assert l1.rpc.getroutes(source=l1.info['id'], + destination=l3.info['id'], + amount_msat=1000000, + layers=[], + maxfee_msat=100000, + final_cltv=0)['probability_ppm'] == initial_prob + + def test_layers(node_factory): """Test manipulating information in layers""" l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) assert l2.rpc.askrene_listlayers() == {'layers': []} - assert l2.rpc.askrene_listlayers('test_layers') == {'layers': []} + with pytest.raises(RpcError, match="Unknown layer"): + l2.rpc.askrene_listlayers('test_layers') expect = {'layer': 'test_layers', 'disabled_nodes': [], 'created_channels': [], + 'channel_updates': [], 'constraints': []} + l2.rpc.askrene_create_layer('test_layers') l2.rpc.askrene_disable_node('test_layers', l1.info['id']) expect['disabled_nodes'].append(l1.info['id']) assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} assert l2.rpc.askrene_listlayers() == {'layers': [expect]} - assert l2.rpc.askrene_listlayers('test_layers2') == {'layers': []} + with pytest.raises(RpcError, match="Unknown layer"): + l2.rpc.askrene_listlayers('test_layers2') + + l2.rpc.askrene_update_channel('test_layers', "0x0x1/0", False) + expect['channel_updates'].append({'short_channel_id_dir': "0x0x1/0", + 'enabled': False}) + assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} + with pytest.raises(RpcError, match="Layer already exists"): + l2.rpc.askrene_create_layer('test_layers') # Tell it l3 connects to l1! l2.rpc.askrene_create_channel('test_layers', l3.info['id'], l1.info['id'], '0x0x1', - '1000000sat', - 100, '900000sat', - 1, 2, 18) - expect['created_channels'].append({'source': l3.info['id'], - 'destination': l1.info['id'], + '1000000sat') + # src/dst gets turned into BOLT 7 order + expect['created_channels'].append({'source': l1.info['id'], + 'destination': l3.info['id'], 'short_channel_id': '0x0x1', - 'capacity_msat': 1000000000, - 'htlc_minimum_msat': 100, - 'htlc_maximum_msat': 900000000, - 'fee_base_msat': 1, - 'fee_proportional_millionths': 2, - 'delay': 18}) + 'capacity_msat': 1000000000}) + assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} + + # And give details. + l2.rpc.askrene_update_channel(layer='test_layers', + short_channel_id_dir='0x0x1/0', + htlc_minimum_msat=100, + htlc_maximum_msat=900000000, + fee_base_msat=1, + fee_proportional_millionths=2, + cltv_expiry_delta=18) + # This is *still* disabled, since we disabled it above! + expect['channel_updates'] = [{'short_channel_id_dir': '0x0x1/0', + 'enabled': False, + 'htlc_minimum_msat': 100, + 'htlc_maximum_msat': 900000000, + 'fee_base_msat': 1, + 'fee_proportional_millionths': 2, + 'cltv_expiry_delta': 18}] + assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} + + # Now enable (and change another value for good measure! + l2.rpc.askrene_update_channel(layer='test_layers', + short_channel_id_dir='0x0x1/0', + enabled=True, + cltv_expiry_delta=19) + expect['channel_updates'] = [{'short_channel_id_dir': '0x0x1/0', + 'enabled': True, + 'htlc_minimum_msat': 100, + 'htlc_maximum_msat': 900000000, + 'fee_base_msat': 1, + 'fee_proportional_millionths': 2, + 'cltv_expiry_delta': 19}] assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} # We can tell it about made up channels... first_timestamp = int(time.time()) l2.rpc.askrene_inform_channel('test_layers', - '0x0x1', - 1, - 100000) + '0x0x1/1', + 100000, + 'unconstrained') last_timestamp = int(time.time()) + 1 - expect['constraints'].append({'short_channel_id': '0x0x1', - 'direction': 1, + expect['constraints'].append({'short_channel_id_dir': '0x0x1/1', 'minimum_msat': 100000}) # Check timestamp first. listlayers = l2.rpc.askrene_listlayers('test_layers') @@ -67,25 +209,24 @@ def test_layers(node_factory): # We can tell it about existing channels... scid12 = first_scid(l1, l2) first_timestamp = int(time.time()) + scid12dir = f"{scid12}/{direction(l2.info['id'], l1.info['id'])}" l2.rpc.askrene_inform_channel(layer='test_layers', - short_channel_id=scid12, - # This is l2 -> l1 - direction=0, - maximum_msat=12341234) + short_channel_id_dir=scid12dir, + amount_msat=12341235, + inform='constrained') last_timestamp = int(time.time()) + 1 - expect['constraints'].append({'short_channel_id': scid12, - 'direction': 0, + expect['constraints'].append({'short_channel_id_dir': scid12dir, 'timestamp': first_timestamp, 'maximum_msat': 12341234}) # Check timestamp first. listlayers = l2.rpc.askrene_listlayers('test_layers') - ts2 = only_one([c['timestamp'] for c in only_one(listlayers['layers'])['constraints'] if c['short_channel_id'] == scid12]) + ts2 = only_one([c['timestamp'] for c in only_one(listlayers['layers'])['constraints'] if c['short_channel_id_dir'] == scid12dir]) assert first_timestamp <= ts2 <= last_timestamp expect['constraints'][1]['timestamp'] = ts2 # Could be either order! actual = expect.copy() - if only_one(listlayers['layers'])['constraints'][0]['short_channel_id'] == scid12: + if only_one(listlayers['layers'])['constraints'][0]['short_channel_id_dir'] == scid12dir: actual['constraints'] = [expect['constraints'][1], expect['constraints'][0]] assert listlayers == {'layers': [actual]} @@ -106,6 +247,12 @@ def test_layers(node_factory): listlayers = l2.rpc.askrene_listlayers('test_layers') assert listlayers == {'layers': [expect]} + with pytest.raises(RpcError, match="Unknown layer"): + l2.rpc.askrene_remove_layer('test_layers_unknown') + + assert l2.rpc.askrene_remove_layer('test_layers') == {} + assert l2.rpc.askrene_listlayers() == {'layers': []} + def check_route_as_expected(routes, paths): """Make sure all fields in paths are match those in routes""" @@ -164,6 +311,46 @@ def test_getroutes(node_factory): # Set up l1 with this as the gossip_store l1 = node_factory.get_node(gossip_store_file=gsfile.name) + # Too much should give a decent explanation. + with pytest.raises(RpcError, match=r"We could not find a usable set of paths\. The shortest path is 0x1x0, but 0x1x0/1 isn't big enough to carry 1000000001msat\."): + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[1], + amount_msat=1000000001, + layers=[], + maxfee_msat=100000000, + final_cltv=99) + + # This should tell us source doesn't have enough. + with pytest.raises(RpcError, match=r"We could not find a usable set of paths\. Total source capacity is only 1019000000msat \(in 3 channels\)\."): + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[1], + amount_msat=2000000001, + layers=[], + maxfee_msat=20000000, + final_cltv=99) + + # This should tell us dest doesn't have enough. + with pytest.raises(RpcError, match=r"We could not find a usable set of paths\. Total destination capacity is only 1000000000msat \(in 1 channels\)\."): + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[4], + amount_msat=1000000001, + layers=[], + maxfee_msat=30000000, + final_cltv=99) + + # Disabling channels makes getroutes fail + l1.rpc.askrene_create_layer('chans_disabled') + l1.rpc.askrene_update_channel(layer="chans_disabled", + short_channel_id_dir='0x1x0/1', + enabled=False) + with pytest.raises(RpcError, match=r"We could not find a usable set of paths\. The shortest path is 0x1x0, but 0x1x0/1 marked disabled by layer chans_disabled\."): + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[1], + amount_msat=1000, + layers=["chans_disabled"], + maxfee_msat=1000, + final_cltv=99) + # Start easy assert l1.rpc.getroutes(source=nodemap[0], destination=nodemap[1], @@ -174,8 +361,7 @@ def test_getroutes(node_factory): 'routes': [{'probability_ppm': 999999, 'final_cltv': 99, 'amount_msat': 1000, - 'path': [{'short_channel_id': '0x1x0', - 'direction': 1, + 'path': [{'short_channel_id_dir': '0x1x0/1', 'next_node_id': nodemap[1], 'amount_msat': 1010, 'delay': 99 + 6}]}]} @@ -189,13 +375,11 @@ def test_getroutes(node_factory): 'routes': [{'probability_ppm': 999798, 'final_cltv': 99, 'amount_msat': 100000, - 'path': [{'short_channel_id': '0x1x0', - 'direction': 1, + 'path': [{'short_channel_id_dir': '0x1x0/1', 'next_node_id': nodemap[1], 'amount_msat': 103020, 'delay': 99 + 6 + 6}, - {'short_channel_id': '1x3x2', - 'direction': 1, + {'short_channel_id_dir': '1x3x2/1', 'next_node_id': nodemap[3], 'amount_msat': 102000, 'delay': 99 + 6} @@ -235,8 +419,7 @@ def test_getroutes(node_factory): 'routes': [{'probability_ppm': 900000, 'final_cltv': 99, 'amount_msat': 1000000, - 'path': [{'short_channel_id': '0x2x3', - 'direction': 1, + 'path': [{'short_channel_id_dir': '0x2x3/1', 'next_node_id': nodemap[2], 'amount_msat': 1000001, 'delay': 99 + 6}]}]} @@ -246,11 +429,11 @@ def test_getroutes(node_factory): nodemap[0], nodemap[2], 10000000, - [[{'short_channel_id': '0x2x1', + [[{'short_channel_id_dir': '0x2x1/1', 'next_node_id': nodemap[2], 'amount_msat': 500000, 'delay': 99 + 6}], - [{'short_channel_id': '0x2x3', + [{'short_channel_id_dir': '0x2x3/1', 'next_node_id': nodemap[2], 'amount_msat': 9500009, 'delay': 99 + 6}]]) @@ -280,8 +463,8 @@ def test_getroutes_fee_fallback(node_factory): nodemap[3], 10000, maxfee_msat=201, - paths=[[{'short_channel_id': '0x1x0'}, - {'short_channel_id': '1x3x2'}]]) + paths=[[{'short_channel_id_dir': '0x1x0/1'}, + {'short_channel_id_dir': '1x3x2/1'}]]) # maxfee exceeded? lower prob path. check_getroute_paths(l1, @@ -289,8 +472,8 @@ def test_getroutes_fee_fallback(node_factory): nodemap[3], 10000, maxfee_msat=200, - paths=[[{'short_channel_id': '0x2x1'}, - {'short_channel_id': '2x3x3'}]]) + paths=[[{'short_channel_id_dir': '0x2x1/1'}, + {'short_channel_id_dir': '2x3x3/0'}]]) def test_getroutes_auto_sourcefree(node_factory): @@ -304,6 +487,21 @@ def test_getroutes_auto_sourcefree(node_factory): # Set up l1 with this as the gossip_store l1 = node_factory.get_node(gossip_store_file=gsfile.name) + # Without sourcefree: + assert l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[1], + amount_msat=1000, + layers=[], + maxfee_msat=1000, + final_cltv=99) == {'probability_ppm': 999999, + 'routes': [{'probability_ppm': 999999, + 'final_cltv': 99, + 'amount_msat': 1000, + 'path': [{'short_channel_id_dir': '0x1x0/1', + 'next_node_id': nodemap[1], + 'amount_msat': 1010, + 'delay': 105}]}]} + # Start easy assert l1.rpc.getroutes(source=nodemap[0], destination=nodemap[1], @@ -314,8 +512,7 @@ def test_getroutes_auto_sourcefree(node_factory): 'routes': [{'probability_ppm': 999999, 'final_cltv': 99, 'amount_msat': 1000, - 'path': [{'short_channel_id': '0x1x0', - 'direction': 1, + 'path': [{'short_channel_id_dir': '0x1x0/1', 'next_node_id': nodemap[1], 'amount_msat': 1000, 'delay': 99}]}]} @@ -329,13 +526,11 @@ def test_getroutes_auto_sourcefree(node_factory): 'routes': [{'probability_ppm': 999798, 'final_cltv': 99, 'amount_msat': 100000, - 'path': [{'short_channel_id': '0x1x0', - 'direction': 1, + 'path': [{'short_channel_id_dir': '0x1x0/1', 'next_node_id': nodemap[1], 'amount_msat': 102000, 'delay': 99 + 6}, - {'short_channel_id': '1x3x2', - 'direction': 1, + {'short_channel_id_dir': '1x3x2/1', 'next_node_id': nodemap[3], 'amount_msat': 102000, 'delay': 99 + 6} @@ -389,15 +584,16 @@ def test_getroutes_auto_localchans(node_factory): final_cltv=99) # This should work + scid21dir = f"{scid12}/{direction(l2.info['id'], l1.info['id'])}" check_getroute_paths(l2, l2.info['id'], nodemap[2], 100000, maxfee_msat=100000, layers=['auto.localchans'], - paths=[[{'short_channel_id': scid12, 'amount_msat': 102012, 'delay': 99 + 6 + 6 + 6}, - {'short_channel_id': '0x1x0', 'amount_msat': 102010, 'delay': 99 + 6 + 6}, - {'short_channel_id': '1x2x1', 'amount_msat': 101000, 'delay': 99 + 6}]]) + paths=[[{'short_channel_id_dir': scid21dir, 'amount_msat': 102012, 'delay': 99 + 6 + 6 + 6}, + {'short_channel_id_dir': '0x1x0/0', 'amount_msat': 102010, 'delay': 99 + 6 + 6}, + {'short_channel_id_dir': '1x2x1/1', 'amount_msat': 101000, 'delay': 99 + 6}]]) # This should get self-discount correct check_getroute_paths(l2, @@ -406,9 +602,9 @@ def test_getroutes_auto_localchans(node_factory): 100000, maxfee_msat=100000, layers=['auto.localchans', 'auto.sourcefree'], - paths=[[{'short_channel_id': scid12, 'amount_msat': 102010, 'delay': 99 + 6 + 6}, - {'short_channel_id': '0x1x0', 'amount_msat': 102010, 'delay': 99 + 6 + 6}, - {'short_channel_id': '1x2x1', 'amount_msat': 101000, 'delay': 99 + 6}]]) + paths=[[{'short_channel_id_dir': scid21dir, 'amount_msat': 102010, 'delay': 99 + 6 + 6}, + {'short_channel_id_dir': '0x1x0/0', 'amount_msat': 102010, 'delay': 99 + 6 + 6}, + {'short_channel_id_dir': '1x2x1/1', 'amount_msat': 101000, 'delay': 99 + 6}]]) def test_fees_dont_exceed_constraints(node_factory): @@ -424,10 +620,11 @@ def test_fees_dont_exceed_constraints(node_factory): l1 = node_factory.get_node(gossip_store_file=gsfile.name) chan = only_one([c for c in l1.rpc.listchannels(source=nodemap[0])['channels'] if c['destination'] == nodemap[1]]) + l1.rpc.askrene_create_layer('test_layers') l1.rpc.askrene_inform_channel(layer='test_layers', - short_channel_id=chan['short_channel_id'], - direction=chan['direction'], - maximum_msat=max_msat) + short_channel_id_dir=f"{chan['short_channel_id']}/{chan['direction']}", + amount_msat=max_msat + 1, + inform='constrained') routes = l1.rpc.getroutes(source=nodemap[0], destination=nodemap[3], @@ -437,7 +634,7 @@ def test_fees_dont_exceed_constraints(node_factory): final_cltv=99)['routes'] assert len(routes) == 2 for hop in routes[0]['path'] + routes[1]['path']: - if hop['short_channel_id'] == chan['short_channel_id']: + if hop['short_channel_id_dir'] == f"{chan['short_channel_id']}/{chan['direction']}": amount = hop['amount_msat'] assert amount <= max_msat @@ -450,13 +647,20 @@ def test_sourcefree_on_mods(node_factory, bitcoind): l1 = node_factory.get_node(gossip_store_file=gsfile.name) # Add a local channel from 0->l1 (we just needed a nodeid). + l1.rpc.askrene_create_layer('test_layers') l1.rpc.askrene_create_channel('test_layers', nodemap[0], l1.info['id'], '0x3x3', - '1000000sat', - 100, '900000sat', - 1000, 2000, 18) + '1000000sat') + l1.rpc.askrene_update_channel(layer='test_layers', + short_channel_id_dir=f'0x3x3/{direction(nodemap[0], l1.info["id"])}', + enabled=True, + htlc_minimum_msat=100, + htlc_maximum_msat='900000sat', + fee_base_msat=1000, + fee_proportional_millionths=2000, + cltv_expiry_delta=18) routes = l1.rpc.getroutes(source=nodemap[0], destination=l1.info['id'], amount_msat=1000000, @@ -464,7 +668,7 @@ def test_sourcefree_on_mods(node_factory, bitcoind): maxfee_msat=100000, final_cltv=99)['routes'] # Expect no fee. - check_route_as_expected(routes, [[{'short_channel_id': '0x3x3', + check_route_as_expected(routes, [[{'short_channel_id_dir': '0x3x3/1', 'amount_msat': 1000000, 'delay': 99}]]) # Same if we specify layers in the other order! @@ -475,7 +679,7 @@ def test_sourcefree_on_mods(node_factory, bitcoind): maxfee_msat=100000, final_cltv=99)['routes'] # Expect no fee. - check_route_as_expected(routes, [[{'short_channel_id': '0x3x3', + check_route_as_expected(routes, [[{'short_channel_id_dir': '0x3x3/1', 'amount_msat': 1000000, 'delay': 99}]]) @@ -517,9 +721,7 @@ def test_live_spendable(node_factory, bitcoind): path_total = {} num_htlcs = {} for r in routes["routes"]: - key = "{}/{}".format( - r["path"][0]["short_channel_id"], r["path"][0]["direction"] - ) + key = r["path"][0]["short_channel_id_dir"] path_total[key] = path_total.get(key, 0) + r["path"][0]["amount_msat"] num_htlcs[key] = num_htlcs.get(key, 0) + 1 @@ -538,9 +740,9 @@ def test_live_spendable(node_factory, bitcoind): # No duplicate paths! for i in range(0, len(routes["routes"])): - path_i = [(p['short_channel_id'], p['direction']) for p in routes["routes"][i]['path']] + path_i = [p['short_channel_id_dir'] for p in routes["routes"][i]['path']] for j in range(i + 1, len(routes["routes"])): - path_j = [(p['short_channel_id'], p['direction']) for p in routes["routes"][j]['path']] + path_j = [p['short_channel_id_dir'] for p in routes["routes"][j]['path']] assert path_i != path_j # Must deliver exact amount. @@ -572,14 +774,18 @@ def test_limits_fake_gossmap(node_factory, bitcoind): for scidd in spendable: assert scidd in [f"{c['short_channel_id']}/{c['direction']}" for c in l1.rpc.listchannels(source=nodemap[0])['channels']] + # We tell it we could get through amount, but not amount + 1. + # This makes min == max, just like we do for auto.localchans spendable. + l1.rpc.askrene_create_layer('localchans') for scidd, amount in spendable.items(): - chan, direction = scidd.split('/') l1.rpc.askrene_inform_channel(layer='localchans', - short_channel_id=chan, direction=int(direction), - minimum_msat=amount) + short_channel_id_dir=scidd, + amount_msat=amount, + inform='unconstrained') l1.rpc.askrene_inform_channel(layer='localchans', - short_channel_id=chan, direction=int(direction), - maximum_msat=amount) + short_channel_id_dir=scidd, + amount_msat=amount + 1, + inform='constrained') routes = l1.rpc.getroutes( source=nodemap[0], @@ -592,9 +798,7 @@ def test_limits_fake_gossmap(node_factory, bitcoind): path_total = {} for r in routes["routes"]: - key = "{}/{}".format( - r["path"][0]["short_channel_id"], r["path"][0]["direction"] - ) + key = r["path"][0]["short_channel_id_dir"] path_total[key] = path_total.get(key, 0) + r["path"][0]["amount_msat"] exceeded = {} @@ -607,9 +811,9 @@ def test_limits_fake_gossmap(node_factory, bitcoind): # No duplicate paths! for i in range(0, len(routes["routes"])): - path_i = [(p['short_channel_id'], p['direction']) for p in routes["routes"][i]['path']] + path_i = [p['short_channel_id_dir'] for p in routes["routes"][i]['path']] for j in range(i + 1, len(routes["routes"])): - path_j = [(p['short_channel_id'], p['direction']) for p in routes["routes"][j]['path']] + path_j = [p['short_channel_id_dir'] for p in routes["routes"][j]['path']] assert path_i != path_j # Must deliver exact amount. @@ -631,16 +835,17 @@ def test_max_htlc(node_factory, bitcoind): final_cltv=10) check_route_as_expected(routes['routes'], - [[{'short_channel_id': '0x1x0', 'amount_msat': 1_000_001, 'delay': 10 + 6}], - [{'short_channel_id': '0x1x1', 'amount_msat': 19_000_019, 'delay': 10 + 6}]]) + [[{'short_channel_id_dir': '0x1x0/1', 'amount_msat': 1_000_001, 'delay': 10 + 6}], + [{'short_channel_id_dir': '0x1x1/1', 'amount_msat': 19_000_019, 'delay': 10 + 6}]]) # If we can't use channel 2, we fail. + l1.rpc.askrene_create_layer('removechan2') l1.rpc.askrene_inform_channel(layer='removechan2', - short_channel_id='0x1x1', direction=1, - maximum_msat=0) + short_channel_id_dir='0x1x1/1', + amount_msat=1, + inform='constrained') - # FIXME: Better diag! - with pytest.raises(RpcError, match="Could not find route"): + with pytest.raises(RpcError, match="We could not find a usable set of paths. The shortest path is 0x1x0, but 0x1x0/1 exceeds htlc_maximum_msat ~1000448msat"): l1.rpc.getroutes(source=nodemap[0], destination=nodemap[1], amount_msat=20_000_000,