diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 003cdbe67..1555768d8 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -76,6 +76,7 @@ from .channel_db import UpdateStatus from .channel_db import get_mychannel_info, get_mychannel_policy from .submarine_swaps import SwapManager from .channel_db import ChannelInfo, Policy +from .mpp_split import suggest_splits if TYPE_CHECKING: from .network import Network @@ -1019,7 +1020,7 @@ class LNWallet(LNWorker): key = payment_hash.hex() payment_secret = lnaddr.payment_secret invoice_pubkey = lnaddr.pubkey.serialize() - invoice_features = lnaddr.get_tag('9') or 0 + invoice_features = LnFeatures(lnaddr.get_tag('9') or 0) r_tags = lnaddr.get_routing_info('r') t_tags = lnaddr.get_routing_info('t') amount_to_pay = lnaddr.get_amount_msat() @@ -1377,6 +1378,16 @@ class LNWallet(LNWorker): node_features=trampoline_features)) return route + def channels_with_funds(self) -> Dict[bytes, int]: + """Determines a dict of channels (keyed by channel id in bytes) that + maps to their spendable amounts.""" + with self.lock: + channels = {} + for cid, chan in self._channels.items(): + spend_amount = int(chan.available_to_spend(HTLCOwner.LOCAL)) + channels[cid] = spend_amount + return channels + @profiler def create_routes_for_payment( self, @@ -1385,11 +1396,83 @@ class LNWallet(LNWorker): min_cltv_expiry, r_tags, invoice_features, - *, full_path: LNPaymentPath = None) -> LNPaymentRoute: - # TODO: return multiples routes if we know that a single one will not work - # initially, try with less htlcs + *, full_path: LNPaymentPath = None) -> Sequence[Tuple[LNPaymentRoute, int]]: + """Creates multiple routes for splitting a payment over the available + private channels. + + We first try to conduct the payment over a single channel. If that fails + and mpp is supported by the receiver, we will split the payment.""" + + try: # to send over a single channel + routes = [self.create_route_for_payment( + amount_msat, + invoice_pubkey, + min_cltv_expiry, + r_tags, + invoice_features, + None, + full_path=full_path + )] + except NoPathFound: + if invoice_features & LnFeatures.BASIC_MPP_OPT: + # Create split configurations that are rated according to our + # preference (low rating=high preference). + split_configurations = suggest_splits( + amount_msat, + self.channels_with_funds() + ) + + self.logger.info("Created the following splitting configurations.") + for s in split_configurations: + self.logger.info(f"{s[0]} rating: {s[1]}") + + routes = [] + for s in split_configurations: + try: + for chanid, part_amount_msat in s[0].items(): + if part_amount_msat: + channel = self.channels[chanid] + # It could happen that the pathfinding uses a channel + # in the graph multiple times, meaning we could exhaust + # its capacity. This could be dealt with by temporarily + # iteratively blacklisting channels for this mpp attempt. + route, amt = self.create_route_for_payment( + part_amount_msat, + invoice_pubkey, + min_cltv_expiry, + r_tags, + invoice_features, + channel, + full_path=None + ) + routes.append((route, amt)) + break + except NoPathFound: + routes = [] + continue + else: + raise + + if not routes: + raise NoPathFound + else: + return routes + + def create_route_for_payment( + self, + amount_msat: int, + invoice_pubkey, + min_cltv_expiry, + r_tags, + invoice_features, + outgoing_channel: Channel = None, + *, full_path: Optional[LNPaymentPath]) -> Tuple[LNPaymentRoute, int]: route = None - channels = list(self.channels.values()) + # we can constrain the payment to a single outgoing channel + if outgoing_channel: + channels = [outgoing_channel] + else: + channels = list(self.channels.values()) scid_to_my_channels = {chan.short_channel_id: chan for chan in channels if chan.short_channel_id is not None} @@ -1465,7 +1548,7 @@ class LNWallet(LNWorker): # add features from invoice route[-1].node_features |= invoice_features # return a list of routes - return [(route, amount_msat)] + return route, amount_msat def add_request(self, amount_sat, message, expiry) -> str: coro = self._add_request_coro(amount_sat, message, expiry) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 1a091080d..b13699cca 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -175,6 +175,7 @@ class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]): htlc_failed = LNWallet.htlc_failed save_preimage = LNWallet.save_preimage get_preimage = LNWallet.get_preimage + create_route_for_payment = LNWallet.create_route_for_payment create_routes_for_payment = LNWallet.create_routes_for_payment create_routes_from_invoice = LNWallet.create_routes_from_invoice _check_invoice = staticmethod(LNWallet._check_invoice) @@ -189,6 +190,7 @@ class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]): channels_for_peer = LNWallet.channels_for_peer _calc_routing_hints_for_invoice = LNWallet._calc_routing_hints_for_invoice handle_error_code_from_failed_htlc = LNWallet.handle_error_code_from_failed_htlc + channels_with_funds = LNWallet.channels_with_funds class MockTransport: