Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion contrib/msggen/msggen/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -16495,7 +16495,11 @@
"",
"Layers are generally maintained by plugins, either to contain persistent information about capacities which have been discovered, or to contain transient information for this particular payment (such as blinded paths or routehints).",
"",
"There are some automatic layers: *auto.localchans* contains information on local channels from this node (including non-public ones), and their exact current spendable capacities. *auto.sourcefree* overrides all channels (including those from previous layers) leading out of the *source* to be zero fee and zero delay. These are both useful in the case where the source is the current node. *auto.include_fees* that fixes the send amount and deducts fee from there, ie. the receiver pays for fees instead of the sender. *auto.no_mpp_support* is deprecated in v26.06: use `maxparts=1` instead."
"There are some automatic layers: *auto.localchans* contains information on local channels from this node (including non-public ones), and their exact current spendable capacities. *auto.sourcefree* overrides all channels (including those from previous layers) leading out of the *source* to be zero fee and zero delay. These are both useful in the case where the source is the current node. *auto.include_fees* that fixes the send amount and deducts fee from there, ie. the receiver pays for fees instead of the sender. *auto.allow_circular* permits *source* to equal *destination*, requesting a circular (self-rebalance) route: see below; it grants permission rather than changing behavior, so it has no effect when *source* and *destination* differ. *auto.no_mpp_support* is deprecated in v26.06: use `maxparts=1` instead.",
"",
"Circular (self-rebalance) routing: normally *source* and *destination* must differ, and *source* == *destination* is rejected as invalid input. If the *auto.allow_circular* layer is included in *layers*, *source* == *destination* is instead treated as a request for a self-rebalance cycle: a route that leaves *source*, traverses the network, and returns to *source*. This is intended for moving liquidity between one's own channels. Combine it with a layer that disables every (*source* -> peer) direction except the channels you want the cycle to drain, and every (peer -> *source*) direction except the channels you want it to fill, to pin both ends of the cycle.",
"",
"In a circular result, the final hop of each route is the real return channel: its *short_channel_id_dir* is the (peer -> *source*) channel direction the cycle returns through, its *node_id_out* is *source* itself, and its *amount_out_msat* is the amount delivered back to *source*. Routes are directly usable for building the payment; no placeholder substitution is required."
],
"categories": [
"readonly"
Expand Down
6 changes: 5 additions & 1 deletion doc/schemas/getroutes.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
"",
"Layers are generally maintained by plugins, either to contain persistent information about capacities which have been discovered, or to contain transient information for this particular payment (such as blinded paths or routehints).",
"",
"There are some automatic layers: *auto.localchans* contains information on local channels from this node (including non-public ones), and their exact current spendable capacities. *auto.sourcefree* overrides all channels (including those from previous layers) leading out of the *source* to be zero fee and zero delay. These are both useful in the case where the source is the current node. *auto.include_fees* that fixes the send amount and deducts fee from there, ie. the receiver pays for fees instead of the sender. *auto.no_mpp_support* is deprecated in v26.06: use `maxparts=1` instead."
"There are some automatic layers: *auto.localchans* contains information on local channels from this node (including non-public ones), and their exact current spendable capacities. *auto.sourcefree* overrides all channels (including those from previous layers) leading out of the *source* to be zero fee and zero delay. These are both useful in the case where the source is the current node. *auto.include_fees* that fixes the send amount and deducts fee from there, ie. the receiver pays for fees instead of the sender. *auto.allow_circular* permits *source* to equal *destination*, requesting a circular (self-rebalance) route: see below; it grants permission rather than changing behavior, so it has no effect when *source* and *destination* differ. *auto.no_mpp_support* is deprecated in v26.06: use `maxparts=1` instead.",
"",
"Circular (self-rebalance) routing: normally *source* and *destination* must differ, and *source* == *destination* is rejected as invalid input. If the *auto.allow_circular* layer is included in *layers*, *source* == *destination* is instead treated as a request for a self-rebalance cycle: a route that leaves *source*, traverses the network, and returns to *source*. This is intended for moving liquidity between one's own channels. Combine it with a layer that disables every (*source* -> peer) direction except the channels you want the cycle to drain, and every (peer -> *source*) direction except the channels you want it to fill, to pin both ends of the cycle.",
"",
"In a circular result, the final hop of each route is the real return channel: its *short_channel_id_dir* is the (peer -> *source*) channel direction the cycle returns through, its *node_id_out* is *source* itself, and its *amount_out_msat* is the amount delivered back to *source*. Routes are directly usable for building the payment; no placeholder substitution is required."
],
"categories": [
"readonly"
Expand Down
264 changes: 261 additions & 3 deletions plugins/askrene/askrene.c
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ static struct command_result *param_layer_names(struct command *cmd,
/* Must be a known layer name */
if (streq((*arr)[i], "auto.localchans")
|| streq((*arr)[i], "auto.sourcefree")
|| streq((*arr)[i], "auto.include_fees"))
|| streq((*arr)[i], "auto.include_fees")
|| streq((*arr)[i], "auto.allow_circular"))
continue;

if (streq((*arr)[i], "auto.no_mpp_support")
Expand Down Expand Up @@ -359,8 +360,223 @@ struct getroutes_info {
/* Non-NULL if we are told to use "auto.localchans" */
struct layer *local_layer;
u32 maxparts;
/* Circular routing (self-rebalance) support.
*
* When the caller passes source == destination (and opts in via
* the auto.allow_circular layer), we splice a node-split into this
* request's
* per-request localmods:
* - Synthesise a fake "us_in" destination node.
* - For every (peer -> source) direction still enabled
* after the caller's layers have been applied, disable
* that real direction and add a mirror (peer -> us_in)
* channel with matching properties.
*
* The MCF then sees a regular s -> t flow problem with
* source -> ... -> peer -> us_in. No direct edge connects
* source to us_in, so the algorithm has to traverse the
* network. Algorithms, flow extraction, and routing-cost
* code run unchanged.
*
* After flow conversion the trailing (peer -> us_in) hop is
* KEPT in each returned route -- its amount_out_msat is the
* actually-delivered amount -- and is translated back to real
* terms at serialization time via `circular_unsplit` below:
* the caller sees the real (peer -> source) return channel
* and their own node id, ready for building sendpay.
*
* Existing callers (source != destination) bypass all of
* this -- do_getroutes only calls inject_circular_fake()
* for source == destination. */

/* Fake-mirror-scid -> real-channel translation table, built by
* inject_circular_fake() and applied by the solver child when
* serializing routes. NULL for normal requests. */
struct circular_unsplit *circular_unsplit;
};

/* Hardcoded fake "us_in" node id for circular routing. Must not
* collide with any real Lightning node id likely to appear in
* gossmap. Lives only in the per-request localmods; collision
* within a single request would be a real-world fluke. Distinct
* from renepay's fake destination (which ends in 0x01). */
static const struct node_id circular_fake_us_in_id = {{
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff,
}};

/* Snapshot of a real (peer -> source) channel we want to mirror.
* Collected while the user's layers are applied to the gossmap, so
* we see the post-mask view and only mirror what the caller has
* left enabled. */
struct circular_mirror {
/* The real (peer -> source) channel direction, to disable. */
struct short_channel_id_dir real_scidd;
/* Peer at the far end of the real channel. */
struct node_id peer_id;
/* Channel properties to copy onto the fake mirror. */
struct amount_msat capacity;
struct amount_msat htlc_min, htlc_max;
struct amount_msat base_fee;
u32 fee_proportional_millionths;
u16 cltv_delta;
};

/* If source == destination (a self-rebalance call), perform
* node-splitting at the gossmap layer:
*
* 1. Apply the caller's layers to the gossmap temporarily so we
* see the post-mask view (i.e. which peer -> source channels
* the caller has left enabled).
* 2. For each still-enabled (peer -> source) direction:
* - Disable the real direction in localmods.
* - Add a mirror (peer -> circular_fake_us_in_id) channel
* with a unique fake scid and the real channel's
* properties copied over.
* 3. Undo the temporary apply, leaving the augmented localmods
* ready for the regular gossmap_apply_localmods() at the end
* of do_getroutes.
* 4. Replace info->dest with circular_fake_us_in_id.
* 5. Record each fake scid -> real channel mapping in
* info->circular_unsplit so the solver child translates the
* final hop of each route back to real terms when
* serializing (see struct circular_unsplit in child/child.h).
*
* Only called for source == destination (do_getroutes guards the
* call). Returns NULL on success, or an error string suitable for
* failing the command. */
static const char *inject_circular_fake(struct getroutes_info *info,
struct gossmap *gossmap,
struct gossmap_localmods *localmods)
{
/* Temporary apply so we can read the user's post-mask view. */
gossmap_apply_localmods(gossmap, localmods);

const struct gossmap_node *me =
gossmap_find_node(gossmap, &info->source);

struct circular_mirror *mirrors =
tal_arr(tmpctx, struct circular_mirror, 0);

if (me) {
for (size_t i = 0; i < me->num_chans; i++) {
int us_half;
const struct gossmap_chan *c =
gossmap_nth_chan(gossmap, me, i, &us_half);
const int peer_to_us_dir = !us_half;

/* Skip directions the caller has masked off. */
if (!c->half[peer_to_us_dir].enabled)
continue;

struct circular_mirror m;
m.real_scidd.scid = gossmap_chan_scid(gossmap, c);
m.real_scidd.dir = peer_to_us_dir;

/* gossmap_nth_node(c, dir) returns the SENDER of
* direction dir. We want the peer at the far end
* of the channel -- i.e. the sender of the
* (peer -> us) direction. Using us_half here
* would return us, not the peer. */
const struct gossmap_node *peer =
gossmap_nth_node(gossmap, c, peer_to_us_dir);
gossmap_node_get_id(gossmap, peer, &m.peer_id);

m.capacity = gossmap_chan_get_capacity(gossmap, c);
m.htlc_min =
gossmap_chan_htlc_min(c, peer_to_us_dir);
m.htlc_max =
gossmap_chan_htlc_max(c, peer_to_us_dir);
m.base_fee =
amount_msat(c->half[peer_to_us_dir].base_fee);
m.fee_proportional_millionths =
c->half[peer_to_us_dir].proportional_fee;
m.cltv_delta = c->half[peer_to_us_dir].delay;

tal_arr_expand(&mirrors, m);
}
}

/* Undo the temporary apply -- the next apply at do_getroutes
* line ~620 will reapply with our augmentations included. */
gossmap_remove_localmods(gossmap, localmods);

/* Add disable + mirror entries to localmods, recording each
* fake scid -> real channel mapping so the solver child can
* translate the final hop back when serializing routes. */
struct circular_unsplit *unsplit = tal(info, struct circular_unsplit);
unsplit->source = info->source;
unsplit->entries = tal_arr(unsplit, struct circular_unsplit_entry, 0);

u32 fake_tx = 0;
for (size_t i = 0; i < tal_count(mirrors); i++) {
const struct circular_mirror *m = &mirrors[i];

/* Disable the real (peer -> source) direction: its inbound
* flow now has to arrive via the mirror into us_in. */
const bool real_enabled = false;
gossmap_local_updatechan(localmods, &m->real_scidd,
&real_enabled,
NULL, NULL, NULL, NULL, NULL);

/* Fake scids count up from 0x0x0. Block 0 cannot hold a
* real mainnet channel, but synthetic gossmaps (and the
* caller's layers, via created channels) can occupy any
* scid, and a clash corrupts that channel instead of
* creating the mirror. Step past scids present in the
* gossmap; gossmap_local_addchan refuses duplicates
* within localmods, covering created channels. */
struct short_channel_id fake_scid;
bool added = false;
while (!added && fake_tx <= 0xFFFFFF) {
if (!mk_short_channel_id(&fake_scid, 0, fake_tx++, 0))
break;
if (gossmap_find_chan(gossmap, &fake_scid))
continue;
added = gossmap_local_addchan(localmods, &m->peer_id,
&circular_fake_us_in_id,
fake_scid, m->capacity,
NULL);
}
if (!added)
return tal_fmt(tmpctx,
"Could not allocate a fake scid to mirror %s",
fmt_short_channel_id_dir(tmpctx,
&m->real_scidd));

const struct short_channel_id_dir mirror_scidd = {
.scid = fake_scid,
.dir = node_id_cmp(&m->peer_id,
&circular_fake_us_in_id) < 0
? 0 : 1,
};
const bool mirror_enabled = true;
if (!gossmap_local_updatechan(localmods, &mirror_scidd,
&mirror_enabled,
&m->htlc_min, &m->htlc_max,
&m->base_fee,
&m->fee_proportional_millionths,
&m->cltv_delta))
return tal_fmt(tmpctx,
"Could not set mirror properties for %s",
fmt_short_channel_id_dir(tmpctx,
&m->real_scidd));

const struct circular_unsplit_entry entry = {
.fake_scid = fake_scid,
.real = m->real_scidd,
};
tal_arr_expand(&unsplit->entries, entry);
}

info->dest = circular_fake_us_in_id;
info->circular_unsplit = unsplit;
return NULL;
}

static void add_layer(const struct layer ***layers,
const struct layer *l,
const struct gossmap *gossmap,
Expand Down Expand Up @@ -402,6 +618,11 @@ static const struct layer **apply_layers(const tal_t *ctx,
/* This layer takes effect when converting flows
* into routes. */
continue;
} else if (streq(layernames[i], "auto.allow_circular")) {
/* This layer takes effect at request setup
* (see inject_circular_fake); nothing to
* apply to the gossmap here. */
continue;
} else {
assert(streq(layernames[i], "auto.sourcefree"));
cmd_log(tmpctx, cmd, LOG_DBG, "Adding auto.sourcefree");
Expand Down Expand Up @@ -616,6 +837,32 @@ static struct command_result *do_getroutes(struct command *cmd,
reserves_clear_capacities(askrene->reserved, askrene->gossmap,
capacities);

/* Self-rebalance handling: when source == destination, splice
* a fake "us_in" destination node into localmods and mirror
* the still-enabled (peer -> source) directions onto it.
* Algorithms see a regular s -> t flow problem and run
* unchanged. Must precede gossmap_apply_localmods so the
* augmented mods are picked up by the apply below. Only
* reachable when the caller passed the auto.allow_circular
* layer (json_getroutes rejects source == destination
* otherwise). */
if (node_id_eq(&info->source, &info->dest)) {
cmd_log(tmpctx, cmd, LOG_DBG,
"source == destination: circular (self-rebalance) "
"route requested");
err = inject_circular_fake(info, askrene->gossmap, localmods);
if (err) {
/* localmods are not applied at this point, so fail
* directly: the fail: path would try to remove
* them. */
return command_fail(cmd, PAY_ROUTE_NOT_FOUND,
"%s", err);
}
cmd_log(tmpctx, cmd, LOG_DBG,
"Circular routing: spliced fake us_in destination "
"node, mirrored peer -> source channels into it");
}

Comment on lines +840 to +865

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this section, isn't it better to have the following pattern: ?

if(node_id_eq(source, dest)){
    // log source==dest -> needs circular ...
    err = inject_circular_fake(...);
    if(err){
        // log needs circular but failed for reason: {err}
        return command_fail(...);
    }
    // log circular routing prepared ...
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in "askrene: make the circular case explicit at the inject_circular_fake call site". This came out cleaner than expected: the info->circular field was only read back for a single debug log, so hoisting the source == destination condition to the call site deletes the field, its initialization, and the post-hoc log entirely. inject_circular_fake() keeps an internal guard as a defensive measure. It also makes the layer structurally inert for normal requests -- nothing circular runs unless source == destination -- which the follow-up commit documents in the schema and pins with an equivalence test.

/* we temporarily apply localmods */
gossmap_apply_localmods(askrene->gossmap, localmods);

Expand Down Expand Up @@ -722,6 +969,7 @@ static struct command_result *do_getroutes(struct command *cmd,
include_next_node_id,
include_amount_msat,
include_delay,
info->circular_unsplit,
replyfds[1]);
abort();
}
Expand Down Expand Up @@ -943,9 +1191,17 @@ static struct command_result *json_getroutes(struct command *cmd,
maxdelay_allowed);
}

if (node_id_eq(source, dest)) {
/* source == destination is invalid input for a normal route, and
* we reject it as such -- unless the caller explicitly opts in to
* circular (self-rebalance) routing via the auto.allow_circular
* layer, in which case we splice a node-split in do_getroutes
* (see inject_circular_fake()) and return a cycle back to source. */
if (node_id_eq(source, dest)
&& !have_layer(info->layers, "auto.allow_circular")) {
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"source and destination must be different");
"source and destination must be different"
" (add the auto.allow_circular layer for"
" a self-rebalance route)");
}

if (command_check_only(cmd))
Expand All @@ -961,6 +1217,8 @@ static struct command_result *json_getroutes(struct command *cmd,
info->dev_algo = *dev_algo;
info->additional_costs = new_htable(info, additional_cost_htable);
info->maxparts = *maxparts;
/* Set by inject_circular_fake() for circular requests. */
info->circular_unsplit = NULL;

if (askrene->num_live_requests >= askrene->max_children) {
cmd_log(tmpctx, cmd, LOG_INFORM,
Expand Down
Loading
Loading