Follow-up from #9150.
layer_apply_constraints() applies impressions without bounding the result:
- reverse direction:
min and max both grow by imp->amount, saturating at UINT64_MAX, with no clamp to the channel's gossmap capacity
- forward direction: both shrink by
imp->amount, saturating at 0
Since #9150, get_constraints() seeds *max from gossmap_chan_get_capacity() instead of -1ULL, so reverse-direction impressions can push max above the channel's real capacity, and enough forward-direction volume drives max to 0 and makes askrene treat a live channel as dead.
The FIXME added to test_xpay_fake_channeld in that PR documents the symptom:
/* FIXME: We fail on #10, due mainly to a buildup of usage on 0x2134x0/0:
* Failed: We could not find a usable set of paths. The shortest path is
* 103x1x0->0x2134x0->1725x11x1725, but 0x2134x0/0 exceeds htlc_maximum_msat ~1000448msat
*/
Bounded in practice by two things: mcf.c defensively does if (min > max) min = max, and xpay calls askrene-age with a one-hour cutoff before every payment, so impressions expire. But the layer state is still incoherent in the meantime, and explain_failure.c documents an invariant (total >= max_capacity_known >= known_usable) that impressions can violate, since max can exceed cap_msat.
Minimum fix: clamp max to the channel capacity and min to max inside layer_apply_constraints().
Broader question, raised by Lagrang3 during review: the magnitude by which impressions move the bars is too aggressive. A successful payment proves liquidity of at least amount existed, so subtracting the full amount from max discards information rather than adding it. Worth revisiting the model alongside the clamp.
Follow-up from #9150.
layer_apply_constraints()applies impressions without bounding the result:minandmaxboth grow byimp->amount, saturating atUINT64_MAX, with no clamp to the channel's gossmap capacityimp->amount, saturating at 0Since #9150,
get_constraints()seeds*maxfromgossmap_chan_get_capacity()instead of-1ULL, so reverse-direction impressions can pushmaxabove the channel's real capacity, and enough forward-direction volume drivesmaxto 0 and makes askrene treat a live channel as dead.The FIXME added to
test_xpay_fake_channeldin that PR documents the symptom:Bounded in practice by two things:
mcf.cdefensively doesif (min > max) min = max, and xpay callsaskrene-agewith a one-hour cutoff before every payment, so impressions expire. But the layer state is still incoherent in the meantime, andexplain_failure.cdocuments an invariant (total >= max_capacity_known >= known_usable) that impressions can violate, sincemaxcan exceedcap_msat.Minimum fix: clamp
maxto the channel capacity andmintomaxinsidelayer_apply_constraints().Broader question, raised by Lagrang3 during review: the magnitude by which impressions move the bars is too aggressive. A successful payment proves liquidity of at least
amountexisted, so subtracting the full amount frommaxdiscards information rather than adding it. Worth revisiting the model alongside the clamp.