lightning icon indicating copy to clipboard operation
lightning copied to clipboard

When htlc min breaks renepay

Open Lagrang3 opened this issue 1 year ago • 0 comments

I came up with the following test case for renepay:

def test_htlc_minmax(node_factory):
    """
    Topology:
    1----2----4
    |         |
    3----5----6

    Try sending a payment from 1 to 6, there are two routes
    1->2->4->6 and 1->3->5->6.
    The route 1->2->4->6 contains a channel with
    htlcmin=htlcmax=X but 0 routing fees, the other path has high routing fees.
    The optimal solution sends n*X along 1->2->4->6 and the rest through
    1->3->5->6.
    """
    opts = [
        {"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},#1
        {"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},#2
        {"disable-mpp": None, "fee-base": 1000, "fee-per-satoshi": 1000},#3
        {"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},#4
        {"disable-mpp": None, "fee-base": 1000, "fee-per-satoshi": 1000},#5
        {"disable-mpp": None, "fee-base": 0, "fee-per-satoshi": 0},#6
    ]
    l1, l2, l3, l4, l5, l6 = node_factory.get_nodes(6, opts=opts)
    channels = start_channels(
        [
            (l1, l2, 1000000),
            (l2, l4, 1000000),
            (l4, l6, 1000000),
            (l1, l3, 1000000),
            (l3, l5, 1000000),
            (l5, l6, 1000000),
        ]
    )
    htlc_max = 100100000
    htlc_min =  99900000
    l4.rpc.setchannel(l6.info["id"], htlcmin=htlc_min, htlcmax=htlc_max)
    l6.rpc.setchannel(l4.info["id"], htlcmin=htlc_min, htlcmax=htlc_max)
    scid = l4.get_channel_scid(l6)
    wait_for(
        lambda: [c["htlc_minimum_msat"] for c in l1.rpc.listchannels(scid)["channels"]]
        == [htlc_min, htlc_min]
    )
    wait_for(
        lambda: [c["htlc_maximum_msat"] for c in l1.rpc.listchannels(scid)["channels"]]
        == [htlc_max, htlc_max]
    )

    # 200000sat (circa) go through 1->2->4->6
    # 50000sat (circa) go through 1->3->5->6
    amt = "250000sat"

    # in the worst case we send 2*99900sat through 1->2->4->6 at 0 fees and the
    # rest 50200sat through 1->3->5->6, paying in total 102451msat in fees.
    worst_case = "250102451msat"

    inv = l6.rpc.invoice(amt, "inv", "description")

    response = l1.rpc.call("renepay", {"invstring": inv["bolt11"]})
    l1.wait_for_htlcs()
    invoice = only_one(l6.rpc.listinvoices("inv")["invoices"])
    assert invoice["amount_received_msat"] >= Millisatoshi(amt)

    # This test would fail if all 250000sat go through 1->3->5->6 paying
    # 502251msat in fees or more.
    assert response["amount_sent_msat"] <= Millisatoshi(worst_case)

The idea is to break renepay's optimality by exploiting a constraint the Min Cost Flow (MCF) algorithm cannot handle. That is, htlc_maximum can be taken into account either by limiting the flow capacity or the arcs, or when dissecting flows into routes. But htlc_minimum has, at least to my knowledge, no way to be set as a constrain in the solver. The htlc_minimum constraint must be taken into account after MCF computation.

Currently in this situation renepay will determine the best route is 1->2->4->6 and since there is an htlc_maximum constraint there, it will construct 3 different payment routes with 100000sat, 100000sat and 50000sat using the same path. Before sendpay is called, the plugin will determine that 50000sat is smaller than htlc_minimum for channel 4->6 and that channel will be flagged as disabled, then MCF will be called again and then the entire payment will be sent through 1->3->5->6, when it would have sufficed to send 50000sat through 1->3->5->6.

Lagrang3 avatar Mar 08 '24 12:03 Lagrang3