From 67d373357b751bf521f6784590598176a2d3b498 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 6 May 2024 20:16:05 +0000 Subject: [PATCH] lnworker: make PaymentFeeBudget defaults configurable - make PaymentFeeBudget proportional fee and flat cutoff fee configurable - closes https://github.com/spesmilo/electrum/issues/7622 - increase flat cutoff fee default to 10 sat - closes https://github.com/spesmilo/electrum/issues/7669 - rm RouteEdge.is_sane_to_use() (per edge limit) and just rely on budgets (per route limit) --- electrum/lnrouter.py | 22 +++------------------- electrum/lnutil.py | 21 ++++++++++++++++++--- electrum/lnworker.py | 2 +- electrum/simple_config.py | 10 ++++++++++ tests/test_lnpeer.py | 2 +- 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py index 06ac795b4..afd813ea3 100644 --- a/electrum/lnrouter.py +++ b/electrum/lnrouter.py @@ -105,16 +105,6 @@ class RouteEdge(PathEdge): cltv_delta=channel_policy.cltv_delta, node_features=node_info.features if node_info else 0) - def is_sane_to_use(self, amount_msat: int) -> bool: - # TODO revise ad-hoc heuristics - # cltv cannot be more than 2 weeks - if self.cltv_delta > 14 * 144: - return False - total_fee = self.fee_for_edge(amount_msat) - if total_fee > get_default_fee_budget_msat(invoice_amount_msat=amount_msat): - return False - return True - def has_feature_varonion(self) -> bool: features = LnFeatures(self.node_features) return features.supports(LnFeatures.VAR_ONION_OPT) @@ -153,7 +143,6 @@ def is_route_within_budget( amt = amount_msat_for_dest cltv_cost_of_route = 0 # excluding cltv_delta_for_dest for route_edge in reversed(route[1:]): - if not route_edge.is_sane_to_use(amt): return False amt += route_edge.fee_for_edge(amt) cltv_cost_of_route += route_edge.cltv_delta fee_cost = amt - amount_msat_for_dest @@ -169,12 +158,6 @@ def is_route_within_budget( return True -def get_default_fee_budget_msat(*, invoice_amount_msat: int) -> int: - # fees <= 1 % of payment are fine - # fees <= 5 sat are fine - return max(5_000, invoice_amount_msat // 100) - - class LiquidityHint: """Encodes the amounts that can and cannot be sent over the direction of a channel. @@ -520,8 +503,9 @@ class LNPathFinder(Logger): start_node=start_node, end_node=end_node, node_info=node_info) - if not route_edge.is_sane_to_use(payment_amt_msat): - return float('inf'), 0 # thanks but no thanks + # Cap cltv of any given edge at 2 weeks (the cost function would not work well for extreme cases) + if route_edge.cltv_delta > 14 * 144: + return float('inf'), 0 # Distance metric notes: # TODO constants are ad-hoc # ( somewhat based on https://github.com/lightningnetwork/lnd/pull/1358 ) # - Edges have a base cost. (more edges -> less likely none will fail) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 102548c58..8d1c020da 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1672,9 +1672,24 @@ class PaymentFeeBudget(NamedTuple): #num_htlc: int @classmethod - def default(cls, *, invoice_amount_msat: int) -> 'PaymentFeeBudget': - from .lnrouter import get_default_fee_budget_msat + def default(cls, *, invoice_amount_msat: int, config: 'SimpleConfig') -> 'PaymentFeeBudget': + millionths_orig = config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS + millionths = min(max(0, millionths_orig), 250_000) # clamp into [0, 25%] + cutoff_orig = config.LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT + cutoff = min(max(0, cutoff_orig), 10_000_000) # clamp into [0, 10k sat] + if millionths != millionths_orig: + _logger.warning( + f"PaymentFeeBudget. found insane fee millionths in config. " + f"clamped: {millionths_orig}->{millionths}") + if cutoff != cutoff_orig: + _logger.warning( + f"PaymentFeeBudget. found insane fee cutoff in config. " + f"clamped: {cutoff_orig}->{cutoff}") + # for small payments, fees <= constant cutoff are fine + # for large payments, the max fee is percentage-based + fee_msat = invoice_amount_msat * millionths // 1_000_000 + fee_msat = max(fee_msat, cutoff) return PaymentFeeBudget( - fee_msat=get_default_fee_budget_msat(invoice_amount_msat=invoice_amount_msat), + fee_msat=fee_msat, cltv=NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE, ) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 5589f4083..33bdb6c14 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1516,7 +1516,7 @@ class LNWallet(LNWorker): f"num_channels={self.channel_db.num_channels}, " f"num_policies={self.channel_db.num_policies}.") self.set_invoice_status(key, PR_INFLIGHT) - budget = PaymentFeeBudget.default(invoice_amount_msat=amount_to_pay) + budget = PaymentFeeBudget.default(invoice_amount_msat=amount_to_pay, config=self.config) success = False try: await self.pay_to_node( diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 3b85373d5..bbaf6b23b 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -1040,6 +1040,16 @@ Note you are at risk of losing the funds in the swap, if the funding transaction This will result in longer routes; it might increase your fees and decrease the success rate of your payments."""), ) INITIAL_TRAMPOLINE_FEE_LEVEL = ConfigVar('initial_trampoline_fee_level', default=1, type_=int) + LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS = ConfigVar( + 'lightning_payment_fee_max_millionths', default=10_000, # 1% + type_=int, + short_desc=lambda: _("Max lightning fees (%) to pay"), + ) + LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT = ConfigVar( + 'lightning_payment_fee_cutoff_msat', default=10_000, # 10 sat + type_=int, + short_desc=lambda: _("Max lightning fees to pay for small payments"), + ) LIGHTNING_NODE_ALIAS = ConfigVar('lightning_node_alias', default='', type_=str) EXPERIMENTAL_LN_FORWARD_PAYMENTS = ConfigVar('lightning_forward_payments', default=False, type_=bool) diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py index 1eae2580d..fa0b8c206 100644 --- a/tests/test_lnpeer.py +++ b/tests/test_lnpeer.py @@ -258,7 +258,7 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): amount_msat=amount_msat, paysession=paysession, full_path=full_path, - budget=PaymentFeeBudget.default(invoice_amount_msat=amount_msat), + budget=PaymentFeeBudget.default(invoice_amount_msat=amount_msat, config=self.config), )] get_payments = LNWallet.get_payments