From f90a08bbe2bb2668b79939ecfa540c00f0ea1485 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 18 May 2022 17:07:18 +0200 Subject: [PATCH] Filter nodes for receiving: - increase MPP_RECEIVE_CUTOFF from 5 to 20 percent - filter channels by node_id, not channel_id - make num_sats_can_receive consistent with routing hints --- electrum/lnworker.py | 71 +++++++++++++++++++++++------------ electrum/tests/test_lnpeer.py | 9 ++--- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 842ad69e9..eecfe5a0f 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -7,6 +7,7 @@ import os from decimal import Decimal import random import time +import operator from typing import (Optional, Sequence, Tuple, List, Set, Dict, TYPE_CHECKING, NamedTuple, Union, Mapping, Any, Iterable, AsyncGenerator, DefaultDict) import threading @@ -91,6 +92,7 @@ if TYPE_CHECKING: SAVED_PR_STATUS = [PR_PAID, PR_UNPAID, PR_SCHEDULED] # status that are persisted +MPP_RECEIVE_CUTOFF = 0.2 NUM_PEERS_TARGET = 4 @@ -1764,18 +1766,12 @@ class LNWallet(LNWorker): assert amount_msat is None or amount_msat > 0 timestamp = int(time.time()) - routing_hints = self.calc_routing_hints_for_invoice(amount_msat) + routing_hints, trampoline_hints = self.calc_routing_hints_for_invoice(amount_msat) if not routing_hints: self.logger.info( "Warning. No routing hints added to invoice. " "Other clients will likely not be able to send to us.") - # if not all hints are trampoline, do not create trampoline invoice invoice_features = self.features.for_invoice() - trampoline_hints = [] - for r in routing_hints: - node_id, short_channel_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta = r[1][0] - if len(r[1])== 1 and self.is_trampoline_peer(node_id): - trampoline_hints.append(('t', (node_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta))) payment_preimage = os.urandom(32) payment_hash = sha256(payment_preimage) info = PaymentInfo(payment_hash, amount_msat, RECEIVED, PR_UNPAID) @@ -2002,16 +1998,12 @@ class LNWallet(LNWorker): def calc_routing_hints_for_invoice(self, amount_msat: Optional[int]): """calculate routing hints (BOLT-11 'r' field)""" routing_hints = [] - channels = list(self.channels.values()) - # do minimal filtering of channels. - # we include channels that cannot *right now* receive (e.g. peer disconnected or balance insufficient) - channels = [chan for chan in channels - if (chan.is_open() and not chan.is_frozen_for_receiving())] - # Filter out channels that have very low receive capacity compared to invoice amt. - # Even with MPP, below a certain threshold, including these channels probably - # hurts more than help, as they lead to many failed attempts for the sender. - channels = [chan for chan in channels - if chan.available_to_spend(REMOTE) > (amount_msat or 0) * 0.05] + with self.lock: + nodes = self.border_nodes_that_can_receive(amount_msat) + channels = [] + for c in self.channels.values(): + if c.node_id in nodes: + channels.append(c) # cap max channels to include to keep QR code reasonably scannable channels = sorted(channels, key=lambda chan: (not chan.is_active(), -chan.available_to_spend(REMOTE))) channels = channels[:15] @@ -2047,7 +2039,12 @@ class LNWallet(LNWorker): fee_base_msat, fee_proportional_millionths, cltv_expiry_delta)])) - return routing_hints + trampoline_hints = [] + for r in routing_hints: + node_id, short_channel_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta = r[1][0] + if len(r[1])== 1 and self.is_trampoline_peer(node_id): + trampoline_hints.append(('t', (node_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta))) + return routing_hints, trampoline_hints def delete_payment(self, payment_hash_hex: str): try: @@ -2085,14 +2082,38 @@ class LNWallet(LNWorker): can_send_minus_fees = max(0, can_send_minus_fees) return Decimal(can_send_minus_fees) / 1000 - def num_sats_can_receive(self) -> Decimal: + def border_nodes_that_can_receive(self, amount_msat=None): + # if amount_msat is None, use the max amount we can receive + # + # Filter out nodes that have very low receive capacity compared to invoice amt. + # Even with MPP, below a certain threshold, including these channels probably + # hurts more than help, as they lead to many failed attempts for the sender. + # + # We condider nodes instead of channels because both non-strict forwardring + # and trampoline end-to-end payments allow it + nodes_that_can_receive = defaultdict(int) with self.lock: - channels = [ - c for c in self.channels.values() - if c.is_active() and not c.is_frozen_for_receiving() - ] - can_receive = sum([c.available_to_spend(REMOTE) for c in channels]) if channels else 0 - return Decimal(can_receive) / 1000 + for c in self.channels.values(): + if not c.is_active() or c.is_frozen_for_receiving(): + continue + nodes_that_can_receive[c.node_id] += c.available_to_spend(REMOTE) + while True: + max_can_receive = sum(nodes_that_can_receive.values()) + receive_amount = amount_msat or max_can_receive + items = sorted(list(nodes_that_can_receive.items()), key=operator.itemgetter(1)) + for node_id, v in items: + if v < receive_amount * MPP_RECEIVE_CUTOFF: + nodes_that_can_receive.pop(node_id) + # break immediately because max_can_receive needs to be recomputed + break + else: + break + return nodes_that_can_receive + + def num_sats_can_receive(self) -> Decimal: + can_receive_nodes = self.border_nodes_that_can_receive(None) + can_receive_msat = sum(can_receive_nodes.values()) + return Decimal(can_receive_msat) / 1000 def num_sats_can_receive_no_mpp(self) -> Decimal: with self.lock: diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index de9bf535a..4340f3131 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -243,6 +243,7 @@ class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]): get_channel_by_id = LNWallet.get_channel_by_id channels_for_peer = LNWallet.channels_for_peer calc_routing_hints_for_invoice = LNWallet.calc_routing_hints_for_invoice + border_nodes_that_can_receive = LNWallet.border_nodes_that_can_receive handle_error_code_from_failed_htlc = LNWallet.handle_error_code_from_failed_htlc is_trampoline_peer = LNWallet.is_trampoline_peer wait_for_received_pending_htlcs_to_get_removed = LNWallet.wait_for_received_pending_htlcs_to_get_removed @@ -485,14 +486,10 @@ class TestPeer(TestCaseForTestnet): w2.save_preimage(RHASH, payment_preimage) w2.save_payment_info(info) if include_routing_hints: - routing_hints = w2.calc_routing_hints_for_invoice(amount_msat) + routing_hints, trampoline_hints = w2.calc_routing_hints_for_invoice(amount_msat) else: routing_hints = [] - trampoline_hints = [] - for r in routing_hints: - node_id, short_channel_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta = r[1][0] - if len(r[1])== 1 and w2.is_trampoline_peer(node_id): - trampoline_hints.append(('t', (node_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta))) + trampoline_hints = [] invoice_features = w2.features.for_invoice() if invoice_features.supports(LnFeatures.PAYMENT_SECRET_OPT): payment_secret = derive_payment_secret_from_payment_preimage(payment_preimage)