Skip to content

Commit 58701dc

Browse files
committed
askrene: detect and cancel flow cycles
Flow cycles can occur if we have arc zero arc costs. The previous path construction from the flow in the network assumed the absence of such cycles and would enter an infinite loop if it hit one. With his patch wee add cycle detection and removal during the path construction phase. Reported-by: Rusty Russell <[email protected]> Signed-off-by: Lagrang3 <[email protected]>
1 parent 5adefd2 commit 58701dc

File tree

1 file changed

+154
-89
lines changed

1 file changed

+154
-89
lines changed

plugins/askrene/mcf.c

Lines changed: 154 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -998,8 +998,10 @@ struct chan_flow
998998
};
999999

10001000
/* Search in the network a path of positive flow until we reach a node with
1001-
* positive balance. */
1002-
static u32 find_positive_balance(
1001+
* positive balance (returns a node idx with positive balance)
1002+
* or we discover a cycle (returns a node idx with 0 balance).
1003+
* */
1004+
static u32 find_path_or_cycle(
10031005
const struct gossmap *gossmap,
10041006
const struct chan_flow *chan_flow,
10051007
const u32 start_idx,
@@ -1009,42 +1011,34 @@ static u32 find_positive_balance(
10091011
int *prev_dir,
10101012
u32 *prev_idx)
10111013
{
1014+
const size_t max_num_nodes = gossmap_max_node_idx(gossmap);
1015+
bitmap *visited =
1016+
tal_arrz(tmpctx, bitmap, BITMAP_NWORDS(max_num_nodes));
10121017
u32 final_idx = start_idx;
1018+
bitmap_set_bit(visited, start_idx);
10131019

1014-
/* TODO(eduardo)
1015-
* This is guaranteed to halt if there are no directed flow cycles.
1016-
* There souldn't be any. In fact if cost is strickly
1017-
* positive, then flow cycles do not exist at all in the
1018-
* MCF solution. But if cost is allowed to be zero for
1019-
* some arcs, then we might have flow cyles in the final
1020-
* solution. We must somehow ensure that the MCF
1021-
* algorithm does not come up with spurious flow cycles. */
1022-
while(balance[final_idx]<=0)
1023-
{
1024-
// printf("%s: node = %d\n",__PRETTY_FUNCTION__,final_idx);
1025-
u32 updated_idx=INVALID_INDEX;
1026-
struct gossmap_node *cur
1027-
= gossmap_node_byidx(gossmap,final_idx);
1020+
/* It is guaranteed to halt, because we either find a node with
1021+
* balance[]>0 or we hit a node twice and we stop. */
1022+
while (balance[final_idx] <= 0) {
1023+
u32 updated_idx = INVALID_INDEX;
1024+
struct gossmap_node *cur =
1025+
gossmap_node_byidx(gossmap, final_idx);
10281026

1029-
for(size_t i=0;i<cur->num_chans;++i)
1030-
{
1027+
for (size_t i = 0; i < cur->num_chans; ++i) {
10311028
int dir;
1032-
const struct gossmap_chan *c
1033-
= gossmap_nth_chan(gossmap,
1034-
cur,i,&dir);
1029+
const struct gossmap_chan *c =
1030+
gossmap_nth_chan(gossmap, cur, i, &dir);
10351031

1036-
if (!gossmap_chan_set(c, dir))
1032+
if (!gossmap_chan_set(c, dir) || !c->half[dir].enabled)
10371033
continue;
10381034

1039-
const u32 c_idx = gossmap_chan_idx(gossmap,c);
1040-
1041-
// follow the flow
1042-
if(chan_flow[c_idx].half[dir]>0)
1043-
{
1044-
const struct gossmap_node *next
1045-
= gossmap_nth_node(gossmap,c,!dir);
1046-
u32 next_idx = gossmap_node_idx(gossmap,next);
1035+
const u32 c_idx = gossmap_chan_idx(gossmap, c);
10471036

1037+
/* follow the flow */
1038+
if (chan_flow[c_idx].half[dir] > 0) {
1039+
const struct gossmap_node *next =
1040+
gossmap_nth_node(gossmap, c, !dir);
1041+
u32 next_idx = gossmap_node_idx(gossmap, next);
10481042

10491043
prev_dir[next_idx] = dir;
10501044
prev_chan[next_idx] = c;
@@ -1055,10 +1049,17 @@ static u32 find_positive_balance(
10551049
}
10561050
}
10571051

1058-
assert(updated_idx!=INVALID_INDEX);
1059-
assert(updated_idx!=final_idx);
1060-
1052+
assert(updated_idx != INVALID_INDEX);
1053+
assert(updated_idx != final_idx);
10611054
final_idx = updated_idx;
1055+
1056+
if (bitmap_test_bit(visited, updated_idx)) {
1057+
/* We have seen this node before, we've found a cycle.
1058+
*/
1059+
assert(balance[updated_idx] <= 0);
1060+
break;
1061+
}
1062+
bitmap_set_bit(visited, updated_idx);
10621063
}
10631064
return final_idx;
10641065
}
@@ -1069,6 +1070,108 @@ struct list_data
10691070
struct flow *flow_path;
10701071
};
10711072

1073+
/* Given a path from a node with negative balance to a node with positive
1074+
* balance, compute the bigest flow and substract it from the nodes balance and
1075+
* the channels allocation. */
1076+
static struct flow *substract_flow(const tal_t *ctx,
1077+
const struct gossmap *gossmap,
1078+
const u32 start_idx, const u32 final_idx,
1079+
s64 *balance, struct chan_flow *chan_flow,
1080+
const u32 *prev_idx, const int *prev_dir,
1081+
const struct gossmap_chan *const *prev_chan)
1082+
{
1083+
assert(balance[start_idx] < 0);
1084+
assert(balance[final_idx] > 0);
1085+
s64 delta = -balance[start_idx];
1086+
size_t length = 0;
1087+
delta = MIN(delta, balance[final_idx]);
1088+
1089+
/* We can only walk backwards, now get me the legth of the path and the
1090+
* max flow we can send through this route. */
1091+
for (u32 cur_idx = final_idx; cur_idx != start_idx;
1092+
cur_idx = prev_idx[cur_idx]) {
1093+
assert(cur_idx != INVALID_INDEX);
1094+
const int dir = prev_dir[cur_idx];
1095+
const struct gossmap_chan *const chan = prev_chan[cur_idx];
1096+
1097+
/* we could optimize here by caching the idx of the channels in
1098+
* the path, but the bottleneck of the algorithm is the MCF
1099+
* computation not here. */
1100+
const u32 chan_idx = gossmap_chan_idx(gossmap, chan);
1101+
1102+
delta = MIN(delta, chan_flow[chan_idx].half[dir]);
1103+
length++;
1104+
}
1105+
1106+
struct flow *f = tal(ctx, struct flow);
1107+
f->path = tal_arr(f, const struct gossmap_chan *, length);
1108+
f->dirs = tal_arr(f, int, length);
1109+
1110+
/* Walk again and substract the flow value (delta). */
1111+
assert(delta > 0);
1112+
balance[start_idx] += delta;
1113+
balance[final_idx] -= delta;
1114+
for (u32 cur_idx = final_idx; cur_idx != start_idx;
1115+
cur_idx = prev_idx[cur_idx]) {
1116+
const int dir = prev_dir[cur_idx];
1117+
const struct gossmap_chan *const chan = prev_chan[cur_idx];
1118+
const u32 chan_idx = gossmap_chan_idx(gossmap, chan);
1119+
1120+
length--;
1121+
/* f->path and f->dirs contain the channels in the path in the
1122+
* correct order. */
1123+
f->path[length] = chan;
1124+
f->dirs[length] = dir;
1125+
1126+
chan_flow[chan_idx].half[dir] -= delta;
1127+
}
1128+
f->delivers = amount_msat(delta * 1000);
1129+
return f;
1130+
}
1131+
1132+
/* Substract a flow cycle from the channel allocation. */
1133+
static void substract_cycle(const struct gossmap *gossmap, const u32 final_idx,
1134+
struct chan_flow *chan_flow, const u32 *prev_idx,
1135+
const int *prev_dir,
1136+
const struct gossmap_chan *const *prev_chan)
1137+
{
1138+
s64 delta = INFINITE;
1139+
u32 cur_idx;
1140+
1141+
/* Compute greatest flow in this cycle. */
1142+
for (cur_idx = final_idx; cur_idx!=INVALID_INDEX;) {
1143+
const int dir = prev_dir[cur_idx];
1144+
const struct gossmap_chan *const chan = prev_chan[cur_idx];
1145+
const u32 chan_idx = gossmap_chan_idx(gossmap, chan);
1146+
1147+
delta = MIN(delta, chan_flow[chan_idx].half[dir]);
1148+
1149+
cur_idx = prev_idx[cur_idx];
1150+
if (cur_idx == final_idx)
1151+
/* we have come back full circle */
1152+
break;
1153+
}
1154+
assert(cur_idx==final_idx);
1155+
1156+
/* Walk again and substract the flow value (delta). */
1157+
assert(delta < INFINITE);
1158+
assert(delta > 0);
1159+
1160+
for (cur_idx = final_idx;cur_idx!=INVALID_INDEX;) {
1161+
const int dir = prev_dir[cur_idx];
1162+
const struct gossmap_chan *const chan = prev_chan[cur_idx];
1163+
const u32 chan_idx = gossmap_chan_idx(gossmap, chan);
1164+
1165+
chan_flow[chan_idx].half[dir] -= delta;
1166+
1167+
cur_idx = prev_idx[cur_idx];
1168+
if (cur_idx == final_idx)
1169+
/* we have come back full circle */
1170+
break;
1171+
}
1172+
assert(cur_idx==final_idx);
1173+
}
1174+
10721175
/* Given a flow in the residual network, build a set of payment flows in the
10731176
* gossmap that corresponds to this flow. */
10741177
static struct flow **
@@ -1092,6 +1195,9 @@ get_flow_paths(const tal_t *ctx,
10921195
int *prev_dir = tal_arr(tmpctx,int,max_num_nodes);
10931196
u32 *prev_idx = tal_arr(tmpctx,u32,max_num_nodes);
10941197

1198+
for (u32 node_idx = 0; node_idx < max_num_nodes; node_idx++)
1199+
prev_idx[node_idx] = INVALID_INDEX;
1200+
10951201
// Convert the arc based residual network flow into a flow in the
10961202
// directed channel network.
10971203
// Compute balance on the nodes.
@@ -1117,75 +1223,34 @@ get_flow_paths(const tal_t *ctx,
11171223

11181224
}
11191225

1120-
11211226
// Select all nodes with negative balance and find a flow that reaches a
11221227
// positive balance node.
11231228
for(u32 node_idx=0;node_idx<max_num_nodes;++node_idx)
11241229
{
1125-
// for(size_t i=0;i<tal_count(prev_idx);++i)
1126-
// {
1127-
// prev_idx[i]=INVALID_INDEX;
1128-
// }
11291230
// this node has negative balance, flows leaves from here
11301231
while(balance[node_idx]<0)
11311232
{
1132-
prev_chan[node_idx]=NULL;
1133-
u32 final_idx = find_positive_balance(
1233+
prev_chan[node_idx] = NULL;
1234+
u32 final_idx = find_path_or_cycle(
11341235
rq->gossmap, chan_flow, node_idx, balance,
11351236
prev_chan, prev_dir, prev_idx);
11361237

1137-
s64 delta=-balance[node_idx];
1138-
int length = 0;
1139-
delta = MIN(delta,balance[final_idx]);
1140-
1141-
// walk backwards, get me the length and the max flow we
1142-
// can send.
1143-
for(u32 cur_idx = final_idx;
1144-
cur_idx!=node_idx;
1145-
cur_idx=prev_idx[cur_idx])
1238+
if (balance[final_idx] > 0)
1239+
/* case 1. found a path */
11461240
{
1147-
assert(cur_idx!=INVALID_INDEX);
1148-
1149-
const int dir = prev_dir[cur_idx];
1150-
const struct gossmap_chan *const c = prev_chan[cur_idx];
1151-
const u32 c_idx = gossmap_chan_idx(rq->gossmap,c);
1152-
1153-
delta=MIN(delta,chan_flow[c_idx].half[dir]);
1154-
length++;
1155-
}
1156-
1157-
struct flow *fp = tal(flows,struct flow);
1158-
fp->path = tal_arr(fp,const struct gossmap_chan *,length);
1159-
fp->dirs = tal_arr(fp,int,length);
1160-
1161-
balance[node_idx] += delta;
1162-
balance[final_idx]-= delta;
1163-
1164-
// walk backwards, substract flow
1165-
for(u32 cur_idx = final_idx;
1166-
cur_idx!=node_idx;
1167-
cur_idx=prev_idx[cur_idx])
1241+
struct flow *fp = substract_flow(
1242+
flows, rq->gossmap, node_idx, final_idx,
1243+
balance, chan_flow, prev_idx, prev_dir,
1244+
prev_chan);
1245+
1246+
tal_arr_expand(&flows, fp);
1247+
} else
1248+
/* case 2. found a cycle */
11681249
{
1169-
assert(cur_idx!=INVALID_INDEX);
1170-
1171-
const int dir = prev_dir[cur_idx];
1172-
const struct gossmap_chan *const c = prev_chan[cur_idx];
1173-
const u32 c_idx = gossmap_chan_idx(rq->gossmap,c);
1174-
1175-
length--;
1176-
fp->path[length]=c;
1177-
fp->dirs[length]=dir;
1178-
// notice: fp->path and fp->dirs have the path
1179-
// in the correct order.
1180-
1181-
chan_flow[c_idx].half[prev_dir[cur_idx]]-=delta;
1250+
substract_cycle(rq->gossmap, final_idx,
1251+
chan_flow, prev_idx, prev_dir,
1252+
prev_chan);
11821253
}
1183-
1184-
assert(delta>0);
1185-
fp->delivers = amount_msat(delta*1000);
1186-
1187-
// add fp to flows
1188-
tal_arr_expand(&flows, fp);
11891254
}
11901255
}
11911256
return flows;

0 commit comments

Comments
 (0)