-
Notifications
You must be signed in to change notification settings - Fork 902
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
pay: Remember and update channel_hints across payments #7494
Merged
+656
−261
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
51ba252
pay: Add a function to update `channel_hint`s based on their age
cdecker 194ce79
route: Add the total capacity to route_hops
cdecker 977b70f
pay: Use the total_mast amount as the upper limit for channel_hints
cdecker a5c20aa
pay: Make the `channel_hint`s global
cdecker e5c719d
make: Weaken over aggressive check-amount-access test
cdecker 2552e5c
plugin: Split out the `struct channel_hint` handling
cdecker 729fe8b
pay: Rename overall_capacity to just capacity
cdecker d918f4c
route: Simplify direction
cdecker ae5329a
pay: Subscribe to the `channel_hint_update` notifications
cdecker 337b452
route: Use safe `amount_sat_to_msat` conversion
cdecker fa9fe09
route: Change the type of the funding capacity to `amount_sat`
cdecker 54e02fe
pytest: Test that we remember learnt channel hints across payments
cdecker 10b3fbb
pay: Use the global `channel_hint_set` and remember across payments
cdecker 257ec32
pay: Add a hysteresis for channel_hint relaxation
cdecker bb7eca8
pay: Add `channel_hint_set_count` primitive
cdecker 3e47249
pay: Log when and why we exclude a channel from the route
cdecker c0b6847
pay: Inject `channel_hint`s we receive via plugin notifications
cdecker 5cd37b5
pay: Remove use of temporary local `channel_hint`
cdecker 9bd1b6f
pytest: Fix up the `test_mutual_connect_race`
cdecker 785778d
test: Fix up the `test_pay_routeboost` test
cdecker 1180eeb
pytest: Fix up the `test_sendpay_grouping` test
cdecker fddadaf
pay: Simplify the `channel_hint` update logic
cdecker 47743bb
route: Re-add the assertion that we're one side of a channel
cdecker d0b2f3c
pay: Switch to msat for total_capacity
cdecker File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
#include "config.h" | ||
#include <plugins/channel_hint.h> | ||
|
||
void channel_hint_to_json(const char *name, const struct channel_hint *hint, | ||
struct json_stream *dest) | ||
{ | ||
json_object_start(dest, name); | ||
json_add_u32(dest, "timestamp", hint->timestamp); | ||
json_add_short_channel_id_dir(dest, "scid", hint->scid); | ||
json_add_amount_msat(dest, "estimated_capacity_msat", | ||
hint->estimated_capacity); | ||
json_add_amount_msat(dest, "total_capacity_msat", hint->capacity); | ||
json_add_bool(dest, "enabled", hint->enabled); | ||
json_object_end(dest); | ||
} | ||
|
||
/* How long until even a channel whose estimate is down at 0msat will | ||
* be considered fully refilled. The refill rate is the inverse of | ||
* this times the channel size. The refilling is a linear | ||
* approximation, with a small hysteresis applied in order to prevent | ||
* a single payment relaxing its own constraints thus causing it to | ||
* prematurely retry an already attempted channel. | ||
*/ | ||
#define PAY_REFILL_TIME 7200 | ||
|
||
/* Add an artificial delay before accepting updates. This ensures we | ||
* don't actually end up relaxing a tight constraint inbetween the | ||
* attempt that added it and the next retry. If we were to relax right | ||
* away, then we could end up retrying the exact same path we just | ||
* failed at. If the `time_between_attempts * refill > 1msat`, we'd | ||
* end up not actually constraining at all, because we set the | ||
* estimate to `attempt - 1msat`. This also results in the updates | ||
* being limited to once every minute, and causes a stairway | ||
* pattern. The hysteresis has to be >60s otherwise a single payment | ||
* can already end up retrying a previously excluded channel. | ||
*/ | ||
#define PAY_REFILL_HYSTERESIS 60 | ||
/** | ||
* Update the `channel_hint` in place, return whether it should be kept. | ||
* | ||
* This computes the refill-rate based on the overall capacity, and | ||
* the time elapsed since the last update and relaxes the upper bound | ||
* on the capacity, and resets the enabled flag if appropriate. If the | ||
* hint is no longer useful, i.e., it does not provide any additional | ||
* information on top of the structural information we've learned from | ||
* the gossip, then we return `false` to signal that the | ||
* `channel_hint` may be removed. | ||
*/ | ||
bool channel_hint_update(const struct timeabs now, struct channel_hint *hint) | ||
{ | ||
/* Precision is not required here, so integer division is good | ||
* enough. But keep the order such that we do not round down | ||
* too much. We do so by first multiplying, before | ||
* dividing. The formula is `current = last + delta_t * | ||
* overall / refill_rate`. | ||
*/ | ||
struct amount_msat refill; | ||
struct amount_msat capacity = hint->capacity; | ||
|
||
if (now.ts.tv_sec < hint->timestamp + PAY_REFILL_HYSTERESIS) | ||
return true; | ||
|
||
u64 seconds = now.ts.tv_sec - hint->timestamp; | ||
if (!amount_msat_mul(&refill, capacity, seconds)) | ||
abort(); | ||
|
||
refill = amount_msat_div(refill, PAY_REFILL_TIME); | ||
if (!amount_msat_add(&hint->estimated_capacity, | ||
hint->estimated_capacity, refill)) | ||
abort(); | ||
|
||
/* Clamp the value to the `overall_capacity` */ | ||
if (amount_msat_greater(hint->estimated_capacity, capacity)) | ||
hint->estimated_capacity = capacity; | ||
|
||
/* TODO This is rather coarse. We could map the disabled flag | ||
to having 0msat capacity, and then relax from there. But it'd | ||
likely be too slow of a relaxation.*/ | ||
if (seconds > 60) | ||
hint->enabled = true; | ||
|
||
/* Since we update in-place we should make sure that we can | ||
* just call update again and the result is stable, if no time | ||
* has passed. */ | ||
hint->timestamp = now.ts.tv_sec; | ||
|
||
/* We report this hint as useless, if the hint does not | ||
* restrict the channel, i.e., if it is enabled and the | ||
* estimate is the same as the overall capacity. */ | ||
return !hint->enabled || | ||
amount_msat_greater(capacity, hint->estimated_capacity); | ||
} | ||
|
||
struct channel_hint *channel_hint_set_find(struct channel_hint_set *self, | ||
const struct short_channel_id_dir *scidd) | ||
{ | ||
for (size_t i=0; i<tal_count(self->hints); i++) { | ||
struct channel_hint *hint = &self->hints[i]; | ||
if (short_channel_id_dir_eq(&hint->scid, scidd)) | ||
return hint; | ||
} | ||
return NULL; | ||
} | ||
|
||
/* See header */ | ||
struct channel_hint * | ||
channel_hint_set_add(struct channel_hint_set *self, u32 timestamp, | ||
const struct short_channel_id_dir *scidd, bool enabled, | ||
const struct amount_msat *estimated_capacity, | ||
const struct amount_msat capacity, u16 *htlc_budget) | ||
{ | ||
struct channel_hint *copy, *old, *newhint; | ||
|
||
/* If the channel is marked as enabled it must have an estimate. */ | ||
assert(!enabled || estimated_capacity != NULL); | ||
|
||
/* If there was no hint, add the new one, if there was one, | ||
* pick the one with the newer timestamp. */ | ||
old = channel_hint_set_find(self, scidd); | ||
copy = tal_dup(tmpctx, struct channel_hint, old); | ||
if (old == NULL) { | ||
newhint = tal(tmpctx, struct channel_hint); | ||
newhint->enabled = enabled; | ||
newhint->scid = *scidd; | ||
newhint->capacity = capacity; | ||
if (estimated_capacity != NULL) | ||
newhint->estimated_capacity = *estimated_capacity; | ||
newhint->local = NULL; | ||
newhint->timestamp = timestamp; | ||
tal_arr_expand(&self->hints, *newhint); | ||
return &self->hints[tal_count(self->hints) - 1]; | ||
} else if (old->timestamp <= timestamp) { | ||
/* `local` is kept, since we do not pass in those | ||
* annotations here. */ | ||
old->enabled = enabled; | ||
old->timestamp = timestamp; | ||
if (estimated_capacity != NULL) | ||
old->estimated_capacity = *estimated_capacity; | ||
|
||
/* We always pick the larger of the capacities we are | ||
* being told. This is because in some cases, such as | ||
* routehints, we're not actually being told the total | ||
* capacity, just lower values. */ | ||
if (amount_msat_greater(capacity, old->capacity)) | ||
old->capacity = capacity; | ||
|
||
return copy; | ||
} else { | ||
return NULL; | ||
} | ||
} | ||
|
||
/** | ||
* Load a channel_hint from its JSON representation. | ||
* | ||
* @return The initialized `channel_hint` or `NULL` if we encountered a parsing | ||
* error. | ||
*/ | ||
struct channel_hint *channel_hint_from_json(const tal_t *ctx, | ||
const char *buffer, | ||
const jsmntok_t *toks) | ||
{ | ||
const char *ret; | ||
const jsmntok_t *payload = json_get_member(buffer, toks, "payload"), | ||
*jhint = | ||
json_get_member(buffer, payload, "channel_hint"); | ||
struct channel_hint *hint = tal(ctx, struct channel_hint); | ||
|
||
ret = json_scan(ctx, buffer, jhint, | ||
"{timestamp:%,scid:%,estimated_capacity_msat:%,total_capacity_msat:%,enabled:%}", | ||
JSON_SCAN(json_to_u32, &hint->timestamp), | ||
JSON_SCAN(json_to_short_channel_id_dir, &hint->scid), | ||
JSON_SCAN(json_to_msat, &hint->estimated_capacity), | ||
JSON_SCAN(json_to_msat, &hint->capacity), | ||
JSON_SCAN(json_to_bool, &hint->enabled)); | ||
|
||
if (ret != NULL) | ||
hint = tal_free(hint); | ||
return hint; | ||
} | ||
|
||
struct channel_hint_set *channel_hint_set_new(const tal_t *ctx) | ||
{ | ||
struct channel_hint_set *set = tal(ctx, struct channel_hint_set); | ||
set->hints = tal_arr(set, struct channel_hint, 0); | ||
return set; | ||
} | ||
|
||
void channel_hint_set_update(struct channel_hint_set *set, | ||
const struct timeabs now) | ||
{ | ||
for (size_t i = 0; i < tal_count(set->hints); i++) | ||
channel_hint_update(time_now(), &set->hints[i]); | ||
} | ||
|
||
size_t channel_hint_set_count(const struct channel_hint_set *set) | ||
{ | ||
return tal_count(set->hints); | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disagree. There's a reason this function was removed. In general, we want to expose values in msat. In this case, the fact that the capacity has to be in whole sats is kind of a detail.