Skip to content

Commit

Permalink
renepay: accomodate fees in the payment flow
Browse files Browse the repository at this point in the history
Min. Cost Flow does not take into account fees when computing a flow
with liquidity constraints.
This is a work-around solution that reduces the amount on every route to
respect the liquidity bound. The deficity in the delivered amount is
solved by running MCF once again.

Changes:

1. the function `flow_complete` allocates amounts to send over the set of routes
   computed by the MCF algorithm, but it does not allocate more than liquidity
   bound of the route. For this reason `minflow` returns a set of routes that
   satisfy the liquidity bounds but it is not guaranteed that the total payment
   reaches the destination therefore there could a deficit in the delivery:
   `deficit = amount_to_deliver - delivering`.

2. in the function `add_payflows` after `minflow` returns a set of routes we
   call `flows_fit_amount` that tries to a allocate the `deficit` in the routes
   that the MCF have computed.

3. if the resulting flows pass all payment constraints then we update
    `amount_to_deliver = amount_to_deliver - delivering`, and the loop
    repeats as long as `amount_to_deliver` is not zero.

In other words, the excess amount, beyond the liquidity bound,
in the routes is removed and then we try to allocate it
into known routes, otherwise we do a whole MCF again just for the
remaining amount.

Fixes issue #6599

ups

added a simple test

fix source

add comments

fix includes in header

add unit test for function channel_maximum_forward
  • Loading branch information
Lagrang3 committed Dec 15, 2023
1 parent 328863e commit 025d047
Show file tree
Hide file tree
Showing 8 changed files with 561 additions and 25 deletions.
331 changes: 331 additions & 0 deletions plugins/renepay/flow.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include <ccan/asort/asort.h>
#include <ccan/tal/str/str.h>
#include <ccan/tal/tal.h>
#include <common/fp16.h>
#include <common/overflows.h>
#include <common/type_to_string.h>
#include <math.h>
#include <plugins/renepay/flow.h>
Expand Down Expand Up @@ -608,6 +610,207 @@ void commit_flow_set(
}
}

/* Based on the knowledge that we have and HTLCs, returns the greatest
* amount that we can send through this channel. */
static struct amount_msat channel_liquidity(
const struct gossmap *gossmap,
struct chan_extra_map *chan_extra_map,
const struct gossmap_chan *chan,
const int dir)
{
const struct chan_extra_half *h
= get_chan_extra_half_by_chan(gossmap,chan_extra_map,chan,dir);
if(!h)
{
plugin_err(pay_plugin->plugin,"%s unexpected chan_extra_half is NULL",
__PRETTY_FUNCTION__);
}
struct amount_msat liquidity = h->known_max;
if(!amount_msat_sub(&liquidity,liquidity,h->htlc_total))
{
plugin_err(pay_plugin->plugin,"%s unexpected, htlc_total>known_max",
__PRETTY_FUNCTION__);
}
return liquidity;
}

/* Checks BOLT 7 HTLC fee condition:
* recv >= base_fee + (send*proportional_fee)/1000000 */
static bool check_fee_inequality(
struct amount_msat recv,
struct amount_msat send,
u64 base_fee,
u64 proportional_fee)
{
// nothing to forward, any incoming amount is good
if(amount_msat_zero(send))
return true;
if(!amount_msat_add_fee(&send,base_fee,proportional_fee))
{
plugin_err(pay_plugin->plugin,"%s Overflow while checking fee inequality",
__PRETTY_FUNCTION__);
}
return amount_msat_greater_eq(recv,send);
}

/* Let `recv` be the maximum amount this channel can receive, this function
* computes the maximum amount this channel can forward `send`.
* From BOLT7 specification wee need to satisfy the following inequality:
*
* recv-send >= base_fee + floor(send*proportional_fee/1000000)
*
* That is equivalent to have
*
* send <= Bound(recv,send)
*
* where
*
* Bound(recv, send) = ((recv - base_fee)*1000000 + (send*proportional_fee) % 1000000)/(proportional_fee+1000000)
*
* However the quantity we want to determine, `send`, appears on both sides of
* the equation. However the term `send*proportional_fee) % 1000000` only
* contributes by increasing the bound by at most one so that we can neglect
* the extra term and use instead
*
* Bound_simple(recv) = ((recv - base_fee)*1000000)/(proportional_fee+1000000)
*
* as the upper bound for `send`. Formally one can check that
*
* Bound_simple(recv) <= Bound(recv, send) < Bound_simple(recv) + 2
*
* So that if one wishes to find the very highest value of `send` that
* satisfies
*
* send <= Bound(recv, send)
*
* it is enough to compute
*
* send = Bound_simple(recv)
*
* which already satisfies the fee equation and then try to go higher
* with send+1, send+2, etc. But we know that it is enough to try up to
* send+1 because Bound(recv, send) < Bound_simple(recv) + 2.
* */
static struct amount_msat channel_maximum_forward(
const struct gossmap_chan *chan,
const int dir,
struct amount_msat recv)
{
const u64 b = chan->half[dir].base_fee,
p = chan->half[dir].proportional_fee;

const u64 one_million = 1000000;
u64 x_msat = recv.millisatoshis; /* Raw: need to invert the fee equation */

// special case, when recv - base_fee <= 0, we cannot forward anything
if(x_msat<=b)
return amount_msat(0);

x_msat -= b;

if(mul_overflows_u64(one_million,x_msat))
{
plugin_err(pay_plugin->plugin,"%s Multiplication overflow",
__PRETTY_FUNCTION__);
}
struct amount_msat best_send= AMOUNT_MSAT_INIT( (one_million*x_msat)/(one_million+p) );

/* Try to increase the value we send until we fail to fulfill the
* fee inequality. It takes only one iteration though. */
for(int i=0;i<10;++i)
{
struct amount_msat next_send;
if(!amount_msat_add(&next_send, best_send, amount_msat(1)))
{
plugin_err(pay_plugin->plugin,"%s amount_msat addition",
__PRETTY_FUNCTION__);
}

if(check_fee_inequality(recv,next_send,b,p))
{
best_send = next_send;
}else
{
break;
}
}
return best_send;
}


static struct amount_msat channel_max_htlc(
const struct gossmap_chan *chan,
const int dir)
{
return amount_msat( fp16_to_u64(chan->half[dir].htlc_max) );
}

/* Returns the greatest amount we can deliver to the destination using this
* route. It takes into account the current knowledge, pending HTLC,
* htlc_max and fees. */
static struct amount_msat flow_maximum_deliverable(
const struct flow *flow,
const struct gossmap *gossmap,
struct chan_extra_map *chan_extra_map)
{
assert(tal_count(flow->path)>0);
assert(tal_count(flow->dirs)>0);
assert(tal_count(flow->path)==tal_count(flow->dirs));
struct amount_msat x
= amount_msat_min(
channel_liquidity(gossmap,chan_extra_map,flow->path[0],flow->dirs[0]),
channel_max_htlc(flow->path[0],flow->dirs[0]));
for(int i=1;i<tal_count(flow->path);++i)
{
// ith node can forward up to 'liquidity_cap' because of the ith channel
// liquidity bound
struct amount_msat liquidity_cap
= channel_liquidity(gossmap,chan_extra_map,flow->path[i],flow->dirs[i]);

/* ith node can receive up to 'x', therefore he will not forward
* more than 'forward_cap' that we compute below inverting the
* fee equation. */
struct amount_msat forward_cap
= channel_maximum_forward(flow->path[i],flow->dirs[i],x);
struct amount_msat x_new
= amount_msat_min(forward_cap,liquidity_cap);
x_new = amount_msat_min(x_new,
channel_max_htlc(flow->path[i],flow->dirs[i]));

if(!amount_msat_less_eq(x_new,x))
{
plugin_err(pay_plugin->plugin,"%s check fail x_new (%s) > x (%s), check forward_cap (%s) and liquidity_cap (%s)",
__PRETTY_FUNCTION__,
fmt_amount_msat(tmpctx,x_new),
fmt_amount_msat(tmpctx,x),
fmt_amount_msat(tmpctx,forward_cap),
fmt_amount_msat(tmpctx,liquidity_cap));
}

// safety check: amounts decrease along the route
assert(amount_msat_less_eq(x_new,x));

struct amount_msat x_check = x_new;

if(!amount_msat_zero(x_new) &&
!amount_msat_add_fee(&x_check,
flow_edge(flow,i)->base_fee,
flow_edge(flow,i)->proportional_fee))
{
plugin_err(pay_plugin->plugin,"%s cannot compute check fees",
__PRETTY_FUNCTION__);
}

// safety check: the max liquidity in the next hop + fees cannot
// be greater than then max liquidity in the current hop, IF the
// next hop is non-zero.
assert( amount_msat_less_eq(x_check,x));

x = x_new;
}
return x;
}

/* Helper function to fill in amounts and success_prob for flow
*
* IMPORTANT: here we do not commit flows to chan_extra, flows are commited
Expand All @@ -623,6 +826,10 @@ void flow_complete(struct flow *flow,
{
flow->success_prob = 1.0;
flow->amounts = tal_arr(flow, struct amount_msat, tal_count(flow->path));

struct amount_msat max_deliverable = flow_maximum_deliverable(flow,gossmap,chan_extra_map);
delivered = amount_msat_min(delivered,max_deliverable);

for (int i = tal_count(flow->path) - 1; i >= 0; i--) {
const struct chan_extra_half *h
= get_chan_extra_half_by_chan(gossmap,
Expand Down Expand Up @@ -892,13 +1099,137 @@ struct amount_msat flow_set_fee(struct flow **flows)
return fee;
}

/* How much do we deliver to destination using this set of routes */
struct amount_msat flow_set_delivers(struct flow **flows)
{
struct amount_msat final = AMOUNT_MSAT(0);
for (size_t i = 0; i < tal_count(flows); i++) {
size_t n = tal_count(flows[i]->amounts);
struct amount_msat this_final = flows[i]->amounts[n-1];

if(!amount_msat_add(&final, this_final,final))
{
plugin_err(pay_plugin->plugin,"%s (line %d) amount_msat overflow",
__PRETTY_FUNCTION__,
__LINE__);
}
}

return final;
}

/* Helper to access the half chan at flow index idx */
const struct half_chan *flow_edge(const struct flow *flow, size_t idx)
{
assert(idx < tal_count(flow->path));
return &flow->path[idx]->half[flow->dirs[idx]];
}

/* Checks if the flows respect the liquidity bounds imposed by the known maximum
* liquidity and pending HTLCs. */
bool check_liquidity_bounds(
struct flow **flows,
const struct gossmap *gossmap,
struct chan_extra_map *chan_extra_map)
{
bool check = true;
for(int i=0;i<tal_count(flows);++i)
{
struct amount_msat max_deliverable
= flow_maximum_deliverable(flows[i],gossmap,chan_extra_map);
struct amount_msat delivers
= flow_delivers(flows[i]);
check &= amount_msat_less_eq(delivers,max_deliverable);
}
return check;
}


/* flows should be a set of optimal routes delivering an amount that is
* slighty less than amount_to_deliver. We will try to reallocate amounts in
* these flows so that it delivers the exact amount_to_deliver to the
* destination.
* Returns how much we are delivering at the end. */
struct amount_msat flows_fit_amount(
struct flow **flows,
struct amount_msat amount_to_deliver,
const struct gossmap *gossmap,
struct chan_extra_map *chan_extra_map)
{
struct amount_msat total_deliver = flow_set_delivers(flows);
if(amount_msat_greater_eq(total_deliver,amount_to_deliver))
return total_deliver;

struct amount_msat deficit;
if(!amount_msat_sub(&deficit,amount_to_deliver,total_deliver))
{
// this should not happen, because we already checked that
// total_deliver<amount_to_deliver
plugin_err(pay_plugin->plugin,
"%s (line %d) cannot substract amount_to_deliver and total_deliver",
__PRETTY_FUNCTION__,
__LINE__);
}
/* TODO: Current algorithm assigns as much of the deficit as possible to the
* list of routes, we can improve this lets say in order to maximize the
* probability. If the deficit is very small with respect to the amount
* each flow carries then optimization here will not make much
* difference. */
for(int i=0;i<tal_count(flows) && !amount_msat_zero(deficit);++i)
{
struct amount_msat max_deliverable
= flow_maximum_deliverable(flows[i],gossmap,chan_extra_map);
struct amount_msat delivers
= flow_delivers(flows[i]);

struct amount_msat diff;
if(!amount_msat_sub(&diff,max_deliverable,delivers))
{
// this should never happen, a precondition of this
// function is that the flows already respect the
// liquidity bounds.
plugin_err(pay_plugin->plugin,
"%s (line %d) cannot substract max_deliverable and delivers",
__PRETTY_FUNCTION__,
__LINE__);
}

if(amount_msat_zero(diff))
continue;

diff = amount_msat_min(diff,deficit);

if(!amount_msat_sub(&deficit,deficit,diff))
{
// this should never happen
plugin_err(pay_plugin->plugin,
"%s (line %d) cannot substract deficit and diff",
__PRETTY_FUNCTION__,
__LINE__);
}
if(!amount_msat_add(&delivers,delivers,diff))
{
plugin_err(pay_plugin->plugin,
"%s (line %d) overflow in addition delivers(%s) + diff (%s)",
__PRETTY_FUNCTION__,
__LINE__,
fmt_amount_msat(tmpctx,delivers),
fmt_amount_msat(tmpctx,diff));
}

flow_complete(flows[i],gossmap,chan_extra_map,delivers);
}
if(!check_liquidity_bounds(flows,gossmap,chan_extra_map))
{
// this should not happen if our algorithm is correct
plugin_err(pay_plugin->plugin,
"%s (line %d) cannot substract amount_to_deliver and delivers",
__PRETTY_FUNCTION__,
__LINE__);
}
return flow_set_delivers(flows);
}

#ifndef SUPERVERBOSE_ENABLED
#undef SUPERVERBOSE
#endif
28 changes: 28 additions & 0 deletions plugins/renepay/flow.h
Original file line number Diff line number Diff line change
Expand Up @@ -322,4 +322,32 @@ void commit_flow_set(
struct chan_extra_map *chan_extra_map,
struct flow **flows);

/* How much do we deliver to destination using this set of routes */
struct amount_msat flow_set_delivers(struct flow **flows);

/* Checks if the flows respect the liquidity bounds imposed by the known maximum
* liquidity and pending HTLCs. */
bool check_liquidity_bounds(
struct flow **flows,
const struct gossmap *gossmap,
struct chan_extra_map *chan_extra_map);

/* flows should be a set of optimal routes delivering an amount that is
* slighty less than amount_to_deliver. We will try to reallocate amounts in
* these flows so that it delivers the exact amount_to_deliver to the
* destination.
* Returns how much we are delivering at the end. */
struct amount_msat flows_fit_amount(
struct flow **flows,
struct amount_msat amount_to_deliver,
const struct gossmap *gossmap,
struct chan_extra_map *chan_extra_map);

/* How much this flow (route with amounts) is delivering to the destination
* node. */
static inline struct amount_msat flow_delivers(const struct flow *flow)
{
return flow->amounts[tal_count(flow->amounts)-1];
}

#endif /* LIGHTNING_PLUGINS_RENEPAY_FLOW_H */

0 comments on commit 025d047

Please sign in to comment.