From 116b71be38077c1ba4c1b5d1794d4f628a70cab9 Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Wed, 4 Jun 2025 07:28:36 +0100 Subject: [PATCH 1/3] common/amount: add ceil division operation on msat Changelog-None Signed-off-by: Lagrang3 --- common/amount.c | 7 ++++ common/amount.h | 6 ++++ common/test/run-amount.c | 70 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/common/amount.c b/common/amount.c index beeaa14b4a65..1eae69cefc8c 100644 --- a/common/amount.c +++ b/common/amount.c @@ -532,6 +532,13 @@ struct amount_msat amount_msat_div(struct amount_msat msat, u64 div) return msat; } +struct amount_msat amount_msat_div_ceil(struct amount_msat msat, u64 div) +{ + u64 res = msat.millisatoshis / div; + msat.millisatoshis = res + (div * res == msat.millisatoshis ? 0 : 1); + return msat; +} + struct amount_sat amount_sat_div(struct amount_sat sat, u64 div) { sat.satoshis /= div; diff --git a/common/amount.h b/common/amount.h index dd6ad61bb262..b1cdbac1d570 100644 --- a/common/amount.h +++ b/common/amount.h @@ -104,7 +104,13 @@ WARN_UNUSED_RESULT bool amount_sat_add_sat_s64(struct amount_sat *val, WARN_UNUSED_RESULT bool amount_msat_accumulate(struct amount_msat *a, struct amount_msat b); +/* returns floor(msat/div) */ struct amount_msat amount_msat_div(struct amount_msat msat, u64 div); + +/* returns ceil(msat/div) */ +struct amount_msat amount_msat_div_ceil(struct amount_msat msat, u64 div); + +/* returns floor(sat/div) */ struct amount_sat amount_sat_div(struct amount_sat sat, u64 div); bool amount_sat_mul(struct amount_sat *res, struct amount_sat sat, u64 mul); diff --git a/common/test/run-amount.c b/common/test/run-amount.c index 5f8a96a0b0bc..0e9f295dc0c2 100644 --- a/common/test/run-amount.c +++ b/common/test/run-amount.c @@ -163,6 +163,75 @@ static void test_amount_with_fee(void) 2100000001234567890ULL); } +static void test_case_amount_div(u64 input, u64 div, u64 expected) +{ + struct amount_msat msat = amount_msat(input); + struct amount_msat expected_msat = amount_msat(expected); + struct amount_msat result_msat = amount_msat_div(msat, div); + assert(amount_msat_eq(result_msat, expected_msat)); +} + +static void test_case_amount_div_ceil(u64 input, u64 div, u64 expected) +{ + struct amount_msat msat = amount_msat(input); + struct amount_msat expected_msat = amount_msat(expected); + struct amount_msat result_msat = amount_msat_div_ceil(msat, div); + assert(amount_msat_eq(result_msat, expected_msat)); +} + +static void test_amount_div(void) +{ + test_case_amount_div(1, 1, 1); + test_case_amount_div(1, 2, 0); + test_case_amount_div(1, 3, 0); + + test_case_amount_div(2, 1, 2); + test_case_amount_div(2, 2, 1); + test_case_amount_div(2, 3, 0); + + test_case_amount_div(3, 1, 3); + test_case_amount_div(3, 2, 1); + test_case_amount_div(3, 3, 1); + test_case_amount_div(3, 4, 0); + + test_case_amount_div(10, 1, 10); + test_case_amount_div(10, 2, 5); + test_case_amount_div(10, 3, 3); + test_case_amount_div(10, 4, 2); + test_case_amount_div(10, 5, 2); + test_case_amount_div(10, 6, 1); + test_case_amount_div(10, 7, 1); + test_case_amount_div(10, 8, 1); + test_case_amount_div(10, 9, 1); + test_case_amount_div(10, 10, 1); + test_case_amount_div(10, 11, 0); + + test_case_amount_div_ceil(1, 1, 1); + test_case_amount_div_ceil(1, 2, 1); + test_case_amount_div_ceil(1, 3, 1); + + test_case_amount_div_ceil(2, 1, 2); + test_case_amount_div_ceil(2, 2, 1); + test_case_amount_div_ceil(2, 3, 1); + + test_case_amount_div_ceil(3, 1, 3); + test_case_amount_div_ceil(3, 2, 2); + test_case_amount_div_ceil(3, 3, 1); + test_case_amount_div_ceil(3, 4, 1); + + test_case_amount_div_ceil(10, 1, 10); + test_case_amount_div_ceil(10, 2, 5); + test_case_amount_div_ceil(10, 3, 4); + test_case_amount_div_ceil(10, 4, 3); + test_case_amount_div_ceil(10, 5, 2); + test_case_amount_div_ceil(10, 6, 2); + test_case_amount_div_ceil(10, 7, 2); + test_case_amount_div_ceil(10, 8, 2); + test_case_amount_div_ceil(10, 9, 2); + test_case_amount_div_ceil(10, 10, 1); + test_case_amount_div_ceil(10, 11, 1); +} + #define FAIL_MSAT(msatp, str) \ assert(!parse_amount_msat((msatp), (str), strlen(str))) #define PASS_MSAT(msatp, str, val) \ @@ -330,5 +399,6 @@ int main(int argc, char *argv[]) } test_amount_with_fee(); + test_amount_div(); common_shutdown(); } From 765c7ec678484d1aca0c51f57117d28a6bab365c Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Mon, 14 Jul 2025 17:08:34 +0100 Subject: [PATCH 2/3] askrene: prune un-used arcs From the multiple arcs that derive from the same channel we consider only those with the smallest cost such that the payment amount and HTLC max can fit in their combined capacity, ie. we prune high cost arcs that surely will never be used by the optimal solution. This reduces the number of arcs in the graph approximately from 8 arcs per channel to approximately 2 arcs per channel. No pruning. amount: 100 1000 10000 100000 1000000 channels: 104741 106163 106607 106654 106666 arcs: 837928 849304 852856 853232 853328 Prune, limit the channel capacity by its HTLC max amount: 100 1000 10000 100000 1000000 channels: 104741 106163 106607 106654 106666 arcs: 255502 259314 260538 260676 260704 Prune, limit the channel capacity to the payment amount amount: 100 1000 10000 100000 1000000 channels: 104741 106163 106607 106654 106666 arcs: 209482 216270 228618 295450 432468 Prune, limit the channel capacity to the payment amount and its HTLC max amount: 100 1000 10000 100000 1000000 channels: 104741 106163 106607 106654 106666 arcs: 209480 212324 213242 215726 228018 This produces a slight speedup for MCF computations: Amount (sats) | speedup ----------------------- 100 | 1.89 1000 | 1.77 10000 | 1.25 100000 | 1.25 1000000 | 1.18 Changelog-None Signed-off-by: Lagrang3 --- plugins/askrene/mcf.c | 18 ++++++++++++++---- tests/test_askrene.py | 8 ++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c index 2c43ae4da78d..4e48431f2133 100644 --- a/plugins/askrene/mcf.c +++ b/plugins/askrene/mcf.c @@ -360,9 +360,18 @@ static void linearize_channel(const struct pay_parameters *params, b = 1 + amount_msat_ratio_floor(maxcap, params->accuracy); /* An extra bound on capacity, here we use it to reduce the flow such - * that it does not exceed htlcmax. */ + * that it does not exceed htlcmax. + * The cap con capacity is not greater than the amount of payment units + * (msat/accuracy). The way a channel is decomposed into linear cost + * arcs (code below) in ascending cost order ensures that the only the + * necessary capacity to forward the payment is allocated in the lower + * cost arcs. This may lead to some arcs in the decomposition (at the + * high cost end) to have a capacity of 0, and we can prune them while + * keeping the solution optimal. */ u64 cap_on_capacity = - amount_msat_ratio_floor(gossmap_chan_htlc_max(c, dir), params->accuracy); + MIN(amount_msat_ratio_floor(gossmap_chan_htlc_max(c, dir), + params->accuracy), + amount_msat_ratio_ceil(params->amount, params->accuracy)); set_capacity(&capacity[0], a, &cap_on_capacity); cost[0]=0; @@ -598,8 +607,9 @@ static void init_linear_network(const tal_t *ctx, // when the `i` hits the `next` node. for(size_t k=0;k Date: Tue, 15 Jul 2025 07:56:31 +0100 Subject: [PATCH 3/3] askrene: mcf: trade granularity for performance Speed in getroutes up by setting the granularity to 1000 Amount (sats) | speedup ----------------------- 100 | 1.00 1000 | 1.00 10000 | 1.06 100000 | 1.31 1000000 | 2.64 Worst runtime of getroutes Amount (sats) | before (ms) | after (ms) -------------------------------------- 100 | 1507 | 761 1000 | 2129 | 1214 10000 | 1632 | 1043 100000 | 2004 | 1150 1000000 | 27170 | 3289 Changelog-None Signed-off-by: Lagrang3 --- plugins/askrene/mcf.c | 11 ++++++++--- tests/test_askrene.py | 6 +++--- tests/test_xpay.py | 25 +++++++++++++------------ 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c index 4e48431f2133..696b810c7574 100644 --- a/plugins/askrene/mcf.c +++ b/plugins/askrene/mcf.c @@ -969,10 +969,15 @@ struct flow **minflow(const tal_t *ctx, params->source = source; params->target = target; params->amount = amount; - params->accuracy = AMOUNT_MSAT(1000); - /* FIXME: params->accuracy = amount_msat_max(amount_msat_div(amount, - * 1000), AMOUNT_MSAT(1)); + /* -> We reduce the granularity of the flow by limiting the subdivision + * of the payment amount into 1000 units of flow. That reduces the + * computational burden for algorithms that depend on it, eg. "capacity + * scaling" and "successive shortest path". + * -> Using Ceil operation instead of Floor so that + * accuracy x 1000 >= amount * */ + params->accuracy = + amount_msat_max(AMOUNT_MSAT(1), amount_msat_div_ceil(amount, 1000)); // template the channel partition into linear arcs params->cap_fraction[0]=0; diff --git a/tests/test_askrene.py b/tests/test_askrene.py index 4c00b411fa8a..01a721e0df69 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -1204,10 +1204,10 @@ def test_real_data(node_factory, bitcoind): # CI, it's slow. if SLOW_MACHINE: limit = 25 - expected = (5, 25, 1567535, 142772, 91) + expected = (6, 25, 1568821, 143649, 91) else: limit = 100 - expected = (9, 96, 6563767, 629671, 91) + expected = (9, 96, 6565467, 630668, 91) fees = {} for n in range(0, limit): @@ -1324,7 +1324,7 @@ def test_real_biases(node_factory, bitcoind): expected = ({1: 6, 2: 6, 4: 7, 8: 12, 16: 14, 32: 19, 64: 25, 100: 25}, 0) else: limit = 100 - expected = ({1: 22, 2: 25, 4: 36, 8: 52, 16: 69, 32: 80, 64: 96, 100: 96}, 0) + expected = ({1: 22, 2: 25, 4: 36, 8: 53, 16: 69, 32: 80, 64: 96, 100: 96}, 0) l1.rpc.askrene_create_layer('biases') num_changed = {} diff --git a/tests/test_xpay.py b/tests/test_xpay.py index ff7179c888dd..d895e51fd593 100644 --- a/tests/test_xpay.py +++ b/tests/test_xpay.py @@ -671,7 +671,7 @@ def test_xpay_bolt12_no_mpp(node_factory, chainparams, deprecations): # Amount needs to be enought that it bothers splitting, but not # so much that it can't pay without mpp. - AMOUNT = 500000000 + AMOUNT = 800000000 # l2 will advertize mpp, l3 won't. l2offer = l2.rpc.offer(AMOUNT, 'test_xpay_bolt12_no_mpp') @@ -686,10 +686,11 @@ def test_xpay_bolt12_no_mpp(node_factory, chainparams, deprecations): assert ret['failed_parts'] == 0 if deprecations: assert ret['successful_parts'] == 2 + assert ret['amount_sent_msat'] == AMOUNT + AMOUNT // 100000 + 2 else: assert ret['successful_parts'] == 1 + assert ret['amount_sent_msat'] == AMOUNT + AMOUNT // 100000 + 1 assert ret['amount_msat'] == AMOUNT - assert ret['amount_sent_msat'] == AMOUNT + AMOUNT // 100000 + 1 def test_xpay_slow_mode(node_factory, bitcoind): @@ -706,18 +707,18 @@ def test_xpay_slow_mode(node_factory, bitcoind): wait_for(lambda: len(l1.rpc.listchannels()['channels']) == 10) # First try an MPP which fails - inv = l5.rpc.invoice(500000000, 'test_xpay_slow_mode_fail', 'test_xpay_slow_mode_fail', preimage='01' * 32)['bolt11'] + inv = l5.rpc.invoice(800000000, 'test_xpay_slow_mode_fail', 'test_xpay_slow_mode_fail', preimage='01' * 32)['bolt11'] l5.rpc.delinvoice('test_xpay_slow_mode_fail', status='unpaid') with pytest.raises(RpcError, match=r"Destination said it doesn't know invoice: incorrect_or_unknown_payment_details"): l1.rpc.xpay(inv) # Now a successful one - inv = l5.rpc.invoice(500000000, 'test_xpay_slow_mode', 'test_xpay_slow_mode', preimage='00' * 32)['bolt11'] + inv = l5.rpc.invoice(800000000, 'test_xpay_slow_mode', 'test_xpay_slow_mode', preimage='00' * 32)['bolt11'] assert l1.rpc.xpay(inv) == {'payment_preimage': '00' * 32, - 'amount_msat': 500000000, - 'amount_sent_msat': 500010002, + 'amount_msat': 800000000, + 'amount_sent_msat': 800016004, 'failed_parts': 0, 'successful_parts': 2} @@ -739,7 +740,7 @@ def test_fail_after_success(node_factory, bitcoind, executor, slow_mode): bitcoind.generate_block(5) wait_for(lambda: len(l1.rpc.listchannels()['channels']) == 10) - inv = l5.rpc.invoice(500000000, 'test_xpay_slow_mode', 'test_xpay_slow_mode', preimage='00' * 32)['bolt11'] + inv = l5.rpc.invoice(800000000, 'test_xpay_slow_mode', 'test_xpay_slow_mode', preimage='00' * 32)['bolt11'] fut = executor.submit(l1.rpc.xpay, invstring=inv, retry_for=0) # Part via l3 is fine. Part via l4 is stuck, so we kill l4 and mine @@ -750,8 +751,8 @@ def test_fail_after_success(node_factory, bitcoind, executor, slow_mode): # Normally, we return as soon as first part succeeds. if slow_mode is False: assert fut.result(TIMEOUT) == {'payment_preimage': '00' * 32, - 'amount_msat': 500000000, - 'amount_sent_msat': 500010002, + 'amount_msat': 800000000, + 'amount_sent_msat': 800016004, 'failed_parts': 0, 'successful_parts': 2} @@ -763,15 +764,15 @@ def test_fail_after_success(node_factory, bitcoind, executor, slow_mode): l1.daemon.wait_for_log(r"UNUSUAL.*Destination accepted partial payment, failed a part \(Error permanent_channel_failure for path ->022d223620a359a47ff7f7ac447c85c46c923da53389221a0054c11c1e3ca31d59->0382ce59ebf18be7d84677c2e35f23294b9992ceca95491fcf8a56c6cb2d9de199->032cf15d1ad9c4a08d26eab1918f732d8ef8fdc6abb9640bf3db174372c491304e, from 022d223620a359a47ff7f7ac447c85c46c923da53389221a0054c11c1e3ca31d59\)") # Could be either way around, check both line = l1.daemon.is_in_log(r"UNUSUAL.*Destination accepted partial payment, failed a part") - assert re.search(r'but accepted only 32000msat of 500000000msat\. Winning\?!', line) or re.search(r'but accepted only 499968000msat of 500000000msat\. Winning\?!', line) + assert re.search(r'but accepted only .* of 800000000msat\. Winning\?!', line) if slow_mode is True: # Now it succeeds, but notes that it only sent one part! res = fut.result(TIMEOUT) # Some variation due to floating point. - assert res['amount_sent_msat'] < 500000000 + assert res['amount_sent_msat'] < 800000000 assert res == {'payment_preimage': '00' * 32, - 'amount_msat': 500000000, + 'amount_msat': 800000000, 'amount_sent_msat': res['amount_sent_msat'], 'failed_parts': 1, 'successful_parts': 1}