diff --git a/plugins/askrene/Makefile b/plugins/askrene/Makefile index dfc719db8..228390068 100644 --- a/plugins/askrene/Makefile +++ b/plugins/askrene/Makefile @@ -1,5 +1,5 @@ -PLUGIN_ASKRENE_SRC := plugins/askrene/askrene.c plugins/askrene/layer.c -PLUGIN_ASKRENE_HEADER := plugins/askrene/askrene.h plugins/askrene/layer.h +PLUGIN_ASKRENE_SRC := plugins/askrene/askrene.c plugins/askrene/layer.c plugins/askrene/reserve.c +PLUGIN_ASKRENE_HEADER := plugins/askrene/askrene.h plugins/askrene/layer.h plugins/askrene/reserve.h PLUGIN_ASKRENE_OBJS := $(PLUGIN_ASKRENE_SRC:.c=.o) $(PLUGIN_ASKRENE_OBJS): $(PLUGIN_ASKRENE_HEADER) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 8011d8f63..d561d15f9 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -16,6 +16,7 @@ #include #include #include +#include #include static struct askrene *get_askrene(struct plugin *plugin) @@ -209,12 +210,26 @@ static struct command_result *json_askrene_reserve(struct command *cmd, { struct reserve_path *path; struct json_stream *response; + size_t num; + struct askrene *askrene = get_askrene(cmd->plugin); if (!param(cmd, buffer, params, p_req("path", param_reserve_path, &path), 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"); + } + response = jsonrpc_stream_success(cmd); return command_finished(cmd, response); } @@ -225,12 +240,27 @@ static struct command_result *json_askrene_unreserve(struct command *cmd, { struct reserve_path *path; struct json_stream *response; + size_t num; + struct askrene *askrene = get_askrene(cmd->plugin); if (!param(cmd, buffer, params, p_req("path", param_reserve_path, &path), 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"); + } + response = jsonrpc_stream_success(cmd); return command_finished(cmd, response); } @@ -456,6 +486,7 @@ static const char *init(struct plugin *plugin, struct askrene *askrene = tal(plugin, struct askrene); askrene->plugin = plugin; list_head_init(&askrene->layers); + askrene->reserved = new_reserve_hash(askrene); askrene->gossmap = gossmap_load(askrene, GOSSIP_STORE_FILENAME, NULL); if (!askrene->gossmap) diff --git a/plugins/askrene/askrene.h b/plugins/askrene/askrene.h index cd8d0f491..02d9e54fd 100644 --- a/plugins/askrene/askrene.h +++ b/plugins/askrene/askrene.h @@ -5,13 +5,6 @@ #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 sciddir; - struct amount_msat amount; -}; - /* A single route. */ struct route { /* Actual path to take */ @@ -26,6 +19,8 @@ struct askrene { struct gossmap *gossmap; /* List of layers */ struct list_head layers; + /* In-flight payment attempts */ + struct reserve_hash *reserved; }; #endif /* LIGHTNING_PLUGINS_ASKRENE_ASKRENE_H */ diff --git a/plugins/askrene/reserve.c b/plugins/askrene/reserve.c new file mode 100644 index 000000000..19ab56c30 --- /dev/null +++ b/plugins/askrene/reserve.c @@ -0,0 +1,119 @@ +#include "config.h" +#include +#include +#include + +/* 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; +} + +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); +} + +HTABLE_DEFINE_TYPE(struct reserve, reserve_scidd, hash_scidd, + reserve_eq_scidd, reserve_hash); + +struct reserve_hash *new_reserve_hash(const tal_t *ctx) +{ + struct reserve_hash *reserved = tal(ctx, struct reserve_hash); + reserve_hash_init(reserved); + return reserved; +} + +/* Find a reservation for this scidd (if any!) */ +const struct reserve *find_reserve(const struct reserve_hash *reserved, + const struct short_channel_id_dir *scidd) +{ + return reserve_hash_get(reserved, scidd); +} + +/* Create a new (empty) reservation */ +static struct reserve *new_reserve(struct reserve_hash *reserved, + const struct short_channel_id_dir *scidd) +{ + struct reserve *r = tal(reserved, struct reserve); + + r->num_htlcs = 0; + r->amount = AMOUNT_MSAT(0); + r->scidd = *scidd; + + reserve_hash_add(reserved, r); + return r; +} + +static void del_reserve(struct reserve_hash *reserved, struct reserve *r) +{ + assert(r->num_htlcs == 0); + + reserve_hash_del(reserved, r); + tal_free(r); +} + +/* Add to existing reservation (false if would overflow). */ +static bool add(struct reserve *r, struct amount_msat amount) +{ + if (!amount_msat_add(&r->amount, r->amount, amount)) + return false; + r->num_htlcs++; + return true; +} + +static bool remove(struct reserve *r, struct amount_msat amount) +{ + if (r->num_htlcs == 0) + return false; + if (!amount_msat_sub(&r->amount, r->amount, amount)) + return false; + r->num_htlcs--; + return true; +} + +/* Atomically add to reserves, or fail. + * Returns offset of failure, or num on success */ +size_t reserves_add(struct reserve_hash *reserved, + const struct short_channel_id_dir *scidds, + const struct amount_msat *amounts, + size_t num) +{ + for (size_t i = 0; i < num; i++) { + struct reserve *r = reserve_hash_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; + } + } + return num; +} + +/* Atomically remove from reserves, to fail. + * Returns offset of failure or tal_count(scidds) */ +size_t reserves_remove(struct reserve_hash *reserved, + const struct short_channel_id_dir *scidds, + const struct amount_msat *amounts, + size_t num) +{ + for (size_t i = 0; i < num; i++) { + struct reserve *r = reserve_hash_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); + } + return num; +} diff --git a/plugins/askrene/reserve.h b/plugins/askrene/reserve.h new file mode 100644 index 000000000..bd8d088bf --- /dev/null +++ b/plugins/askrene/reserve.h @@ -0,0 +1,38 @@ +#ifndef LIGHTNING_PLUGINS_ASKRENE_RESERVE_H +#define LIGHTNING_PLUGINS_ASKRENE_RESERVE_H +/* We have to know what payments are in progress, so we can take into + * account the reduced capacity of channels. We do this by telling + * everyone to reserve / unreserve paths as they use them. */ +#include "config.h" +#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_hash *new_reserve_hash(const tal_t *ctx); + +/* Find a reservation for this scidd (if any!) */ +const struct reserve *find_reserve(const struct reserve_hash *reserved, + const struct short_channel_id_dir *scidd); + +/* Atomically add to reserves, or fail. + * Returns offset of failure, or num on success */ +size_t reserves_add(struct reserve_hash *reserved, + const struct short_channel_id_dir *scidds, + const struct amount_msat *amounts, + size_t num); + +/* Atomically remove from reserves, to fail. + * Returns offset of failure or tal_count(scidds) */ +size_t reserves_remove(struct reserve_hash *reserved, + const struct short_channel_id_dir *scidds, + const struct amount_msat *amounts, + size_t num); + +#endif /* LIGHTNING_PLUGINS_ASKRENE_RESERVE_H */