Skip to content

Commit f2b884e

Browse files
committed
askrene: flexible liquidity bounds
Liquidity bounds are estimated starting from the observations gathered from askrene-inform-channel but are also evolved in time, so that older entries have less weight in the outcome than recent ones. Also when combined, these intel entries are relaxed base on the likelyhood of the observation with the prior knowledge, eg. observing a channel failure when Pickhardt-Richer probability of success is 99% indicates that it is likely that our prior knowledge was wrong. Changelog-Fixed: askrene-getroutes: liquidity bounds evolve with time and with the evidence gathered from askrene-inform-channel Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
1 parent 2fa718b commit f2b884e

7 files changed

Lines changed: 237 additions & 23 deletions

File tree

plugins/askrene/child/mcf.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,9 @@ static bool channel_is_available(const struct route_query *rq,
357357
* @low: the liquidity is known to be greater or equal than "low"
358358
* @high: the liquidity is known to be less than "high"
359359
* @amount: how much is required to forward */
360-
static double pickhardt_richter_probability(struct amount_msat low,
361-
struct amount_msat high,
362-
struct amount_msat amount)
360+
double pickhardt_richter_probability(struct amount_msat low,
361+
struct amount_msat high,
362+
struct amount_msat amount)
363363
{
364364
struct amount_msat all_states, good_states;
365365
if (amount_msat_greater_eq(amount, high))

plugins/askrene/child/mcf.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <common/jsonrpc_errors.h>
1010

1111
struct route_query;
12+
struct flow;
1213

1314
/* A wrapper to the min. cost flow solver that actually takes into consideration
1415
* the extra msats per channel needed to pay for fees. */
@@ -33,4 +34,13 @@ const char *single_path_routes(const tal_t *ctx, struct route_query *rq,
3334
double *probability,
3435
enum jsonrpc_errcode *ecode);
3536

37+
/* The probability of forwarding a payment amount given a high and low liquidity
38+
* bounds.
39+
* @low: the liquidity is known to be greater or equal than "low"
40+
* @high: the liquidity is known to be less than "high"
41+
* @amount: how much is required to forward */
42+
double pickhardt_richter_probability(struct amount_msat low,
43+
struct amount_msat high,
44+
struct amount_msat amount);
45+
3646
#endif /* LIGHTNING_PLUGINS_ASKRENE_CHILD_MCF_H */

plugins/askrene/child/route_query.c

Lines changed: 184 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
#include "config.h"
2+
#include <ccan/asort/asort.h>
23
#include <common/gossmap.h>
4+
#include <math.h>
35
#include <plugins/askrene/child/additional_costs.h>
6+
#include <plugins/askrene/child/mcf.h>
47
#include <plugins/askrene/child/route_query.h>
58
#include <plugins/askrene/layer.h>
69
#include <plugins/askrene/reserve.h>
710

11+
/* Lifetime of liquidity bounds is one day. "Lifetime" in the sense of the
12+
* exponential time decay: the time it takes for the liquidity lower bound to be
13+
* reduced by half is "lifetime" times ln(2) ~ 16 hours. */
14+
#define ASKRENE_RELAX_TIME_SECS 86400
15+
16+
/* It could be any number between 0 and 1. It represents the fraction of lower
17+
* liquidity bound that we adjust when we find a failure. The smaller it is the
18+
* more we trust previous knowledge. Similar to a "learning velocity" for AI. */
19+
#define ASKRENE_FAILURE_RELAX_FRACTION 0.5
20+
821
struct amount_msat get_additional_per_htlc_cost(const struct route_query *rq,
922
const struct short_channel_id_dir *scidd)
1023
{
@@ -16,6 +29,169 @@ struct amount_msat get_additional_per_htlc_cost(const struct route_query *rq,
1629
return AMOUNT_MSAT(0);
1730
}
1831

32+
static int constraint_cmp(const struct constraint *a,
33+
const struct constraint *b, void *unused)
34+
{
35+
if (a->timestamp < b->timestamp)
36+
return -1;
37+
if (a->timestamp > b->timestamp)
38+
return 1;
39+
return 0;
40+
}
41+
42+
/* Like a capacitor discharging, this is the physical process of information
43+
* getting older and entropy increasing. It satisfies the semigroup property so
44+
* it is a well defined Markovian time evolution operation. Same for the
45+
* "charge" operation.
46+
* Dicharge(x, t) = x * exp(-t/lifetime)
47+
* Charge(x, t) = C - (C -x) * exp(-t/lifetime)
48+
*
49+
* Discharge(x, t1+t2) = Discharge(Discharge(x, t1), t2),
50+
* Charge(x, t1+t2) = Charge(Charge(x, t1), t2),
51+
*
52+
* @min: apply discharge to it,
53+
* @max: apply charge to it,
54+
* @capacity: "capacitor"'s capacity,
55+
* @time_delta: time interval in seconds, 0-> nothing changes, infinity->full
56+
* charge/discharge
57+
* @lifetime: characteristic time of the system in seconds, ie. min is reduced
58+
* by half after ln(2)*lifetime seconds.
59+
*/
60+
// FIXME: unit test
61+
static void exponential_time_charge_discharge(struct amount_msat *min,
62+
struct amount_msat *max,
63+
const struct amount_msat capacity,
64+
const u64 time_delta,
65+
const u64 lifetime)
66+
{
67+
double factor = exp((-1.0 * time_delta) / lifetime);
68+
struct amount_msat residual;
69+
70+
if (!amount_msat_scale(min, *min, factor))
71+
goto fail;
72+
73+
if (!amount_msat_sub(&residual, capacity, *max))
74+
goto fail;
75+
if (!amount_msat_scale(&residual, residual, factor))
76+
goto fail;
77+
if (!amount_msat_sub(max, capacity, residual))
78+
goto fail;
79+
80+
fail:
81+
/* It should not fail, but if it does, we default to 0 knowledge. */
82+
*min = AMOUNT_MSAT(0);
83+
*max = capacity;
84+
}
85+
86+
/* Computes min/max bounds based on known constraints. It self-adjusts for
87+
* contradictory information giving precedence to more recent constraints. Time
88+
* decay is considered.
89+
* FIXME: this approach was completey cooked by hand because it is better than
90+
* simply trusting all constraints as we have seen during tests (see CLN #9282).
91+
* However it would be nice to have a theoretically sound adjustment, eg.
92+
* Maximum Likelyhood, if applicable. */
93+
// FIXME: unit test
94+
static void constraints_get_bounds_selfadjust(struct constraint *constraints,
95+
const struct amount_msat capacity,
96+
struct amount_msat *min,
97+
struct amount_msat *max)
98+
{
99+
assert(ASKRENE_FAILURE_RELAX_FRACTION >= 0.0 &&
100+
ASKRENE_FAILURE_RELAX_FRACTION <= 1.0);
101+
u64 last_timestamp = 0, delta;
102+
double prob_fail;
103+
struct amount_msat x, amount, high;
104+
105+
*min = AMOUNT_MSAT(0);
106+
*max = capacity;
107+
asort(constraints, tal_count(constraints), constraint_cmp, NULL);
108+
for (size_t i = 0; i < tal_count(constraints); i++) {
109+
assert(constraints[i].timestamp >= last_timestamp);
110+
delta = constraints[i].timestamp - last_timestamp;
111+
last_timestamp = constraints[i].timestamp;
112+
113+
/* time relax the bound we carry */
114+
exponential_time_charge_discharge(min, max, capacity, delta,
115+
ASKRENE_RELAX_TIME_SECS);
116+
117+
if (amount_msat_greater_eq(constraints[i].max,
118+
AMOUNT_MSAT(UINT64_MAX))) {
119+
/* this is an "unconstrained" event, a min value bound
120+
*/
121+
x = constraints[i].min;
122+
*min = amount_msat_max(*min, x);
123+
*max = amount_msat_max(*max, x);
124+
} else {
125+
/* this is a "constrained" event, a max value bound */
126+
x = constraints[i].max;
127+
if (amount_msat_greater(x, *max)) {
128+
/* Trivial case, we were expecting x to fail. */
129+
} else if (amount_msat_less(x, *min)) {
130+
/* This should have succeeded 100% of the times,
131+
* our knowledge was wrong. */
132+
*min = amount_msat_min(*min, x);
133+
*max = amount_msat_min(*max, x);
134+
if (!amount_msat_scale(
135+
min, *min,
136+
1.0 - ASKRENE_FAILURE_RELAX_FRACTION)) {
137+
*min = AMOUNT_MSAT(0);
138+
}
139+
} else {
140+
/* We got failure for a quantity between min and
141+
* max bounds. We relax a little the lower bound
142+
* in relation to the probability of this event
143+
* taking place. If p~1 this was expected,
144+
* min/max reflected reality. On the other hand
145+
* if p~0, we were either unlucky or more likely
146+
* our lower bound was too high. */
147+
148+
/* off-by-one because the high bound in MCF
149+
* means "we know the liquidity is below this
150+
* value", which makes some equations take a
151+
* simpler form. */
152+
if (!amount_msat_add(&high, *max,
153+
AMOUNT_MSAT(1)))
154+
high = capacity;
155+
/* off-by-one because
156+
* json_askrene_inform_channel already
157+
* substracted 1msat here, meaning we tried x+1
158+
* and it failed. */
159+
if (!amount_msat_add(&amount, x,
160+
AMOUNT_MSAT(1)))
161+
amount = capacity;
162+
prob_fail = 1.0 - pickhardt_richter_probability(
163+
*min, high, amount);
164+
assert(prob_fail >= 0 && prob_fail <= 1.0);
165+
166+
*max = amount_msat_min(*max, x);
167+
if (!amount_msat_scale(
168+
min, *min,
169+
1.0 + ASKRENE_FAILURE_RELAX_FRACTION *
170+
(prob_fail - 1.0))) {
171+
*min = AMOUNT_MSAT(0);
172+
}
173+
}
174+
}
175+
}
176+
}
177+
178+
/* Constraints produced bounds this way since askrene was first written. We keep
179+
* it around momentarily. */
180+
// static void constraints_get_bounds_legacy(const struct constraint *constraints,
181+
// const struct amount_msat capacity,
182+
// struct amount_msat *min,
183+
// struct amount_msat *max)
184+
// {
185+
// *min = AMOUNT_MSAT(0);
186+
// *max = capacity;
187+
// for (size_t i = 0; i < tal_count(constraints); i++) {
188+
// *min = amount_msat_max(*min, constraints[i].min);
189+
// *max = amount_msat_min(*max, constraints[i].max);
190+
// }
191+
// if(amount_msat_greater(*min, *max))
192+
// *min = *max;
193+
// }
194+
19195
void get_constraints(const struct route_query *rq,
20196
const struct gossmap_chan *chan,
21197
int dir,
@@ -24,28 +200,29 @@ void get_constraints(const struct route_query *rq,
24200
{
25201
struct short_channel_id_dir scidd;
26202
size_t idx = gossmap_chan_idx(rq->gossmap, chan);
27-
28-
*min = AMOUNT_MSAT(0);
203+
struct constraint *constraints = tal_arr(rq, struct constraint, 0);
29204

30205
/* Fast path: no information known, no reserve. */
31206
if (idx < tal_count(rq->capacities) && rq->capacities[idx] != 0) {
207+
*min = AMOUNT_MSAT(0);
32208
*max = amount_msat(fp16_to_u64(rq->capacities[idx]) * 1000);
33209
return;
34210
}
35211

212+
const struct amount_msat capacity =
213+
gossmap_chan_get_capacity(rq->gossmap, chan);
214+
36215
/* Naive implementation! */
37216
scidd.scid = gossmap_chan_scid(rq->gossmap, chan);
38217
scidd.dir = dir;
39-
*max = AMOUNT_MSAT(-1ULL);
40218

41219
/* Look through layers for any constraints (might be dummy
42220
* ones, for created channels!) */
43221
for (size_t i = 0; i < tal_count(rq->layers); i++)
44-
layer_apply_constraints(rq->layers[i], &scidd, min, max);
222+
constraints = layer_get_constraints(rq, rq->layers[i], &scidd,
223+
take(constraints));
45224

46-
/* Might be here because it's reserved, but capacity is normal. */
47-
if (amount_msat_eq(*max, AMOUNT_MSAT(-1ULL)))
48-
*max = gossmap_chan_get_capacity(rq->gossmap, chan);
225+
constraints_get_bounds_selfadjust(constraints, capacity, min, max);
49226

50227
/* Finally, if any is in use, subtract that! */
51228
reserve_sub(rq->reserved, &scidd, rq->layers, min);

plugins/askrene/layer.c

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,6 @@ struct local_update {
3434
const struct amount_msat *htlc_min, *htlc_max;
3535
};
3636

37-
/* A constraint reflects something we learned about a channel */
38-
struct constraint {
39-
struct short_channel_id_dir scidd;
40-
/* Time this constraint was last updated */
41-
u64 timestamp;
42-
/* Non-zero means set */
43-
struct amount_msat min;
44-
/* Non-0xFFFFF.... means set */
45-
struct amount_msat max;
46-
};
47-
4837
/* A bias, for special-effects (user-controlled) */
4938
struct bias {
5039
struct short_channel_id_dir scidd;
@@ -1010,6 +999,24 @@ void layer_apply_constraints(const struct layer *layer,
1010999
}
10111000
}
10121001

1002+
struct constraint *
1003+
layer_get_constraints(const tal_t *ctx, const struct layer *layer,
1004+
const struct short_channel_id_dir *scidd,
1005+
struct constraint *in_constraints TAKES)
1006+
{
1007+
struct constraint *c;
1008+
struct constraint_hash_iter cit;
1009+
struct constraint *constraints =
1010+
tal_dup_talarr(ctx, struct constraint, in_constraints);
1011+
1012+
/* We can have more than one: apply them all! */
1013+
for (c = constraint_hash_getfirst(layer->constraints, scidd, &cit); c;
1014+
c = constraint_hash_getnext(layer->constraints, scidd, &cit)) {
1015+
tal_arr_expand(&constraints, *c);
1016+
}
1017+
return constraints;
1018+
}
1019+
10131020
const struct constraint *layer_add_constraint(struct layer *layer,
10141021
const struct short_channel_id_dir *scidd,
10151022
u64 timestamp,

plugins/askrene/layer.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ struct command;
1818
struct layer;
1919
struct json_stream;
2020

21+
/* A constraint reflects something we learned about a channel */
22+
struct constraint {
23+
struct short_channel_id_dir scidd;
24+
/* Time this constraint was last updated */
25+
u64 timestamp;
26+
/* Non-zero means set */
27+
struct amount_msat min;
28+
/* Non-0xFFFFF.... means set */
29+
struct amount_msat max;
30+
};
31+
2132
/* Create a layer hash table */
2233
struct layer_name_hash *new_layer_name_hash(const tal_t *ctx);
2334

@@ -102,6 +113,13 @@ void layer_apply_constraints(const struct layer *layer,
102113
struct amount_msat *max)
103114
NO_NULL_ARGS;
104115

116+
/* Fetches from this layer all constraints entries that match the scidd
117+
* provided. */
118+
struct constraint *
119+
layer_get_constraints(const tal_t *ctx, const struct layer *layer,
120+
const struct short_channel_id_dir *scidd,
121+
struct constraint *in_constraints TAKES);
122+
105123
/* Apply biases from a layer. */
106124
void layer_apply_biases(const struct layer *layer,
107125
const struct gossmap *gossmap,

plugins/xpay/xpay.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131

3232
#define PREIMAGE_TLV_TYPE 5482373484
3333

34+
/* Entries older than 1 week are thrown away. */
35+
#define XPAY_AGE_TIME_SECS 604800
36+
3437
/* For the whole plugin */
3538
struct xpay {
3639
/* This is me. */
@@ -2966,7 +2969,7 @@ static struct command_result *age_layer(struct command *cmd, struct payment *pay
29662969
plugin_broken_cb,
29672970
payment);
29682971
json_add_string(req->js, "layer", "xpay");
2969-
json_add_u64(req->js, "cutoff", clock_time().ts.tv_sec - 3600);
2972+
json_add_u64(req->js, "cutoff", clock_time().ts.tv_sec - XPAY_AGE_TIME_SECS);
29702973
return send_outreq(req);
29712974
}
29722975

tests/test_xpay.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,6 @@ def test_xpay_selfpay(node_factory):
228228
canned_gossmap_badnodes = [19, 53, 69, 72, 86]
229229

230230

231-
@pytest.mark.skip(reason="askrene-getroutes breaks after the fakenet upgrade")
232231
@pytest.mark.slow_test
233232
@unittest.skipIf(TEST_NETWORK != 'regtest', '29-way split for node 17 is too dusty on elements')
234233
@pytest.mark.parametrize("slow_mode", [False, True])

0 commit comments

Comments
 (0)