diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py index 9fd6d329b..167134b99 100644 --- a/electrum/lnrouter.py +++ b/electrum/lnrouter.py @@ -153,6 +153,9 @@ def is_route_sane_to_use(route: LNPaymentRoute, invoice_amount_msat: int, min_fi # TODO revise ad-hoc heuristics if cltv > NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE: return False + # FIXME in case of MPP, the fee checks are done independently for each part, + # which is ok for the proportional checks but not for the absolute ones. + # This is not that big of a deal though as we don't split into *too many* parts. if not is_fee_sane(total_fee, payment_amount_msat=invoice_amount_msat): return False return True diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 5e44a44b2..8777d5626 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -83,7 +83,7 @@ from .channel_db import UpdateStatus, ChannelDBNotLoaded 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 +from .mpp_split import suggest_splits, SplitConfigRating from .trampoline import create_trampoline_route_and_onion, TRAMPOLINE_FEES, is_legacy_relay if TYPE_CHECKING: @@ -661,6 +661,8 @@ class LNWallet(LNWorker): MPP_EXPIRY = 120 TIMEOUT_SHUTDOWN_FAIL_PENDING_HTLCS = 3 # seconds PAYMENT_TIMEOUT = 120 + MPP_SPLIT_PART_FRACTION = 0.2 + MPP_SPLIT_PART_MINAMT_MSAT = 5_000_000 def __init__(self, wallet: 'Abstract_Wallet', xprv): self.wallet = wallet @@ -1604,12 +1606,21 @@ class LNWallet(LNWorker): else: return random.choice(list(hardcoded_trampoline_nodes().values())).pubkey - def suggest_splits(self, amount_msat: int, my_active_channels, invoice_features, r_tags): + def suggest_splits( + self, + *, + amount_msat: int, + final_total_msat: int, + my_active_channels: Sequence[Channel], + invoice_features: LnFeatures, + r_tags, + ) -> List['SplitConfigRating']: channels_with_funds = { (chan.channel_id, chan.node_id): int(chan.available_to_spend(HTLCOwner.LOCAL)) for chan in my_active_channels } self.logger.info(f"channels_with_funds: {channels_with_funds}") + exclude_single_part_payments = False if self.uses_trampoline(): # in the case of a legacy payment, we don't allow splitting via different # trampoline nodes, because of https://github.com/ACINQ/eclair/issues/2127 @@ -1621,10 +1632,15 @@ class LNWallet(LNWorker): else: exclude_multinode_payments = False exclude_single_channel_splits = False + if invoice_features.supports(LnFeatures.BASIC_MPP_OPT) and not self.config.TEST_FORCE_DISABLE_MPP: + # if amt is still large compared to total_msat, split it: + if (amount_msat / final_total_msat > self.MPP_SPLIT_PART_FRACTION + and amount_msat > self.MPP_SPLIT_PART_MINAMT_MSAT): + exclude_single_part_payments = True split_configurations = suggest_splits( amount_msat, channels_with_funds, - exclude_single_part_payments=False, + exclude_single_part_payments=exclude_single_part_payments, exclude_multinode_payments=exclude_multinode_payments, exclude_single_channel_splits=exclude_single_channel_splits ) @@ -1664,7 +1680,13 @@ class LNWallet(LNWorker): chan.is_active() and not chan.is_frozen_for_sending()] # try random order random.shuffle(my_active_channels) - split_configurations = self.suggest_splits(amount_msat, my_active_channels, invoice_features, r_tags) + split_configurations = self.suggest_splits( + amount_msat=amount_msat, + final_total_msat=final_total_msat, + my_active_channels=my_active_channels, + invoice_features=invoice_features, + r_tags=r_tags, + ) for sc in split_configurations: is_multichan_mpp = len(sc.config.items()) > 1 is_mpp = sum(len(x) for x in list(sc.config.values())) > 1 @@ -1672,6 +1694,8 @@ class LNWallet(LNWorker): continue if not is_mpp and self.config.TEST_FORCE_MPP: continue + if is_mpp and self.config.TEST_FORCE_DISABLE_MPP: + continue self.logger.info(f"trying split configuration: {sc.config.values()} rating: {sc.rating}") routes = [] try: diff --git a/electrum/simple_config.py b/electrum/simple_config.py index a840baa6d..971324f3c 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -869,6 +869,7 @@ class SimpleConfig(Logger): TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = ConfigVar('test_fail_htlcs_with_temp_node_failure', default=False, type_=bool) TEST_FAIL_HTLCS_AS_MALFORMED = ConfigVar('test_fail_malformed_htlc', default=False, type_=bool) TEST_FORCE_MPP = ConfigVar('test_force_mpp', default=False, type_=bool) + TEST_FORCE_DISABLE_MPP = ConfigVar('test_force_disable_mpp', default=False, type_=bool) TEST_SHUTDOWN_FEE = ConfigVar('test_shutdown_fee', default=None, type_=int) TEST_SHUTDOWN_FEE_RANGE = ConfigVar('test_shutdown_fee_range', default=None) TEST_SHUTDOWN_LEGACY = ConfigVar('test_shutdown_legacy', default=False, type_=bool) diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh index e32141945..079d835df 100755 --- a/electrum/tests/regtest/regtest.sh +++ b/electrum/tests/regtest/regtest.sh @@ -93,6 +93,7 @@ if [[ $1 == "init" ]]; then $agent setconfig --offline use_gossip True $agent setconfig --offline server 127.0.0.1:51001:t $agent setconfig --offline lightning_to_self_delay 144 + $agent setconfig --offline test_force_disable_mpp True # alice is funded, bob is listening if [[ $2 == "bob" ]]; then $bob setconfig --offline lightning_listen localhost:9735 diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 544e0d9ed..87cabc95d 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -133,6 +133,8 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): PAYMENT_TIMEOUT = 120 TIMEOUT_SHUTDOWN_FAIL_PENDING_HTLCS = 0 INITIAL_TRAMPOLINE_FEE_LEVEL = 0 + MPP_SPLIT_PART_FRACTION = 1 # this disables the forced splitting + MPP_SPLIT_PART_MINAMT_MSAT = 5_000_000 def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_queue, name): self.name = name