diff --git a/electrum/fee_policy.py b/electrum/fee_policy.py index cfb1909c4..f09f458ca 100644 --- a/electrum/fee_policy.py +++ b/electrum/fee_policy.py @@ -11,7 +11,9 @@ from .logging import Logger if TYPE_CHECKING: from .network import Network -FEE_ETA_TARGETS = [25, 10, 5, 2, 1] +# 1008 = max conf target of core's estimatesmartfee, requesting more results in rpc error. +# estimatesmartfee guarantees that the fee will get accepted into the mempool +FEE_ETA_TARGETS = [1008, 144, 25, 10, 5, 2, 1] FEE_DEPTH_TARGETS = [10_000_000, 5_000_000, 2_000_000, 1_000_000, 800_000, 600_000, 400_000, 250_000, 100_000] FEERATE_STATIC_VALUES = [1000, 2000, 5000, 10000, 20000, 30000, @@ -27,8 +29,10 @@ FEERATE_MAX_RELAY = 50000 # warn user if fee/amount for on-chain tx is higher than this FEE_RATIO_HIGH_WARNING = 0.05 -FEE_LN_ETA_TARGET = 2 # note: make sure the network is asking for estimates for this target -FEE_LN_LOW_ETA_TARGET = 25 # note: make sure the network is asking for estimates for this target +# note: make sure the network is asking for estimates for these targets +FEE_LN_ETA_TARGET = 2 +FEE_LN_LOW_ETA_TARGET = 25 +FEE_LN_MINIMUM_ETA_TARGET = 1008 # The min feerate_per_kw that can be used in lightning so that @@ -149,6 +153,10 @@ class FeePolicy(Logger): return _('Low fee') elif x == 1: return _('In the next block') + elif x == 144: + return _('Within one day') + elif x == 1008: + return _("Within one week") else: return _('Within {} blocks').format(x) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index c98bf4c4b..a0bbad99b 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -55,7 +55,7 @@ from .interface import GracefulDisconnect from .lnrouter import fee_for_edge_msat from .json_db import StoredDict from .invoices import PR_PAID -from .fee_policy import FEE_LN_ETA_TARGET, FEERATE_PER_KW_MIN_RELAY_LIGHTNING +from .fee_policy import FEE_LN_ETA_TARGET, FEE_LN_MINIMUM_ETA_TARGET, FEERATE_PER_KW_MIN_RELAY_LIGHTNING from .trampoline import decode_routing_info if TYPE_CHECKING: @@ -994,7 +994,9 @@ class Peer(Logger, EventListener): raise Exception('Not a trampoline node: ' + str(self.their_features)) channel_flags = CF_ANNOUNCE_CHANNEL if public else 0 - feerate: Optional[int] = self.lnworker.current_target_feerate_per_kw() + feerate: Optional[int] = self.lnworker.current_target_feerate_per_kw( + has_anchors=self.use_anchors() + ) if feerate is None: raise NoDynamicFeeEstimates() # we set a channel type for internal bookkeeping @@ -1603,7 +1605,8 @@ class Peer(Logger, EventListener): return if self.network.blockchain().is_tip_stale() \ or not self.lnworker.wallet.is_up_to_date() \ - or self.lnworker.current_target_feerate_per_kw() is None: + or self.lnworker.current_target_feerate_per_kw(has_anchors=chan.has_anchors()) \ + is None: # don't try to reestablish until we can do fee estimation and are up-to-date return # if we get here, we will try to do a proper reestablish @@ -2605,51 +2608,66 @@ class Peer(Logger, EventListener): return if chan.get_state() != ChannelState.OPEN: return - feerate_per_kw: Optional[int] = self.lnworker.current_target_feerate_per_kw() - if feerate_per_kw is None: + current_feerate_per_kw: Optional[int] = self.lnworker.current_target_feerate_per_kw( + has_anchors=chan.has_anchors() + ) + if current_feerate_per_kw is None: return + # add some buffer to anchor chan fees as we always act at the lower end and don't + # want to get kicked out of the mempool immediately if it grows + fee_buffer = current_feerate_per_kw * 0.5 if chan.has_anchors() else 0 + update_feerate_per_kw = int(current_feerate_per_kw + fee_buffer) def does_chan_fee_need_update(chan_feerate: Union[float, int]) -> Optional[bool]: - # We raise fees more aggressively than we lower them. Overpaying is not too bad, - # but lowballing can be fatal if we can't even get into the mempool... - high_fee = 2 * feerate_per_kw # type: Union[float, int] - low_fee = self.lnworker.current_low_feerate_per_kw() # type: Optional[Union[float, int]] - if low_fee is None: - return None - low_fee = max(low_fee, 0.75 * feerate_per_kw) - # make sure low_feerate and target_feerate are not too close to each other: - low_fee = min(low_fee, feerate_per_kw - FEERATE_PER_KW_MIN_RELAY_LIGHTNING) - assert low_fee < high_fee, (low_fee, high_fee) - return not (low_fee < chan_feerate < high_fee) + if chan.has_anchors(): + # TODO: once package relay and electrum servers with submitpackage are more common, + # TODO: we should reconsider this logic and move towards 0 fee ctx + # update if we used up half of the buffer or the fee decreased a lot again + fee_increased = current_feerate_per_kw + (fee_buffer / 2) > chan_feerate + changed_significantly = abs((chan_feerate - update_feerate_per_kw) / chan_feerate) > 0.2 + return fee_increased or changed_significantly + else: + # We raise fees more aggressively than we lower them. Overpaying is not too bad, + # but lowballing can be fatal if we can't even get into the mempool... + high_fee = 2 * current_feerate_per_kw # type: # Union[float, int] + low_fee = self.lnworker.current_low_feerate_per_kw_srk_channel() # type: Optional[Union[float, int]] + if low_fee is None: + return None + low_fee = max(low_fee, 0.75 * current_feerate_per_kw) + # make sure low_feerate and target_feerate are not too close to each other: + low_fee = min(low_fee, current_feerate_per_kw - FEERATE_PER_KW_MIN_RELAY_LIGHTNING) + assert low_fee < high_fee, (low_fee, high_fee) + return not (low_fee < chan_feerate < high_fee) if not chan.constraints.is_initiator: if constants.net is not constants.BitcoinRegtest: chan_feerate = chan.get_latest_feerate(LOCAL) - ratio = chan_feerate / feerate_per_kw + ratio = chan_feerate / update_feerate_per_kw if ratio < 0.5: # Note that we trust the Electrum server about fee rates # Thus, automated force-closing might not be a good idea # Maybe we should display something in the GUI instead self.logger.warning( f"({chan.get_id_for_log()}) feerate is {chan_feerate} sat/kw, " - f"current recommended feerate is {feerate_per_kw} sat/kw, consider force closing!") + f"current recommended feerate is {update_feerate_per_kw} sat/kw, consider force closing!") return # it is our responsibility to update the fee chan_fee = chan.get_next_feerate(REMOTE) if does_chan_fee_need_update(chan_fee): self.logger.info(f"({chan.get_id_for_log()}) onchain fees have changed considerably. updating fee.") elif chan.get_latest_ctn(REMOTE) == 0: - # workaround eclair issue https://github.com/ACINQ/eclair/issues/1730 + # workaround eclair issue https://github.com/ACINQ/eclair/issues/1730 (fixed in 2022) self.logger.info(f"({chan.get_id_for_log()}) updating fee to bump remote ctn") - if feerate_per_kw == chan_fee: - feerate_per_kw += 1 + if current_feerate_per_kw == chan_fee: + update_feerate_per_kw += 1 else: return self.logger.info(f"({chan.get_id_for_log()}) current pending feerate {chan_fee}. " - f"new feerate {feerate_per_kw}") - chan.update_fee(feerate_per_kw, True) + f"new feerate {update_feerate_per_kw}") + assert update_feerate_per_kw >= FEERATE_PER_KW_MIN_RELAY_LIGHTNING, f"fee below minimum: {update_feerate_per_kw}" + chan.update_fee(update_feerate_per_kw, True) self.send_message( "update_fee", channel_id=chan.channel_id, - feerate_per_kw=feerate_per_kw) + feerate_per_kw=update_feerate_per_kw) self.maybe_send_commitment(chan) @log_exceptions diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 7be4673a1..70695cd66 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -39,7 +39,8 @@ from .util import ( UnrelatedTransactionException, LightningHistoryItem ) from .fee_policy import FeePolicy, FixedFeePolicy -from .fee_policy import FEERATE_FALLBACK_STATIC_FEE, FEE_LN_ETA_TARGET, FEE_LN_LOW_ETA_TARGET, FEERATE_PER_KW_MIN_RELAY_LIGHTNING +from .fee_policy import (FEERATE_FALLBACK_STATIC_FEE, FEE_LN_ETA_TARGET, FEE_LN_LOW_ETA_TARGET, + FEERATE_PER_KW_MIN_RELAY_LIGHTNING, FEE_LN_MINIMUM_ETA_TARGET) from .invoices import Invoice, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED, LN_EXPIRY_NEVER, BaseInvoice from .bitcoin import COIN, opcodes, make_op_return, address_to_scripthash, DummyAddress from .bip32 import BIP32Node @@ -3034,16 +3035,22 @@ class LNWallet(LNWorker): else: await self.taskgroup.spawn(self.reestablish_peer_for_given_channel(chan)) - def current_target_feerate_per_kw(self) -> Optional[int]: + def current_target_feerate_per_kw(self, *, has_anchors: bool) -> Optional[int]: if self.network.fee_estimates.has_data(): - feerate_per_kvbyte = self.network.fee_estimates.eta_target_to_fee(FEE_LN_ETA_TARGET) + target: int = FEE_LN_MINIMUM_ETA_TARGET if has_anchors else FEE_LN_ETA_TARGET + feerate_per_kvbyte = self.network.fee_estimates.eta_target_to_fee(target) + if has_anchors: + # set a floor of 5 sat/vb to have some safety margin in case the mempool + # grows quickly + feerate_per_kvbyte = max(feerate_per_kvbyte, 5000) else: if constants.net is not constants.BitcoinRegtest: return None feerate_per_kvbyte = FEERATE_FALLBACK_STATIC_FEE return max(FEERATE_PER_KW_MIN_RELAY_LIGHTNING, feerate_per_kvbyte // 4) - def current_low_feerate_per_kw(self) -> Optional[int]: + def current_low_feerate_per_kw_srk_channel(self) -> Optional[int]: + """Gets low feerate for static remote key channels.""" if constants.net is constants.BitcoinRegtest: feerate_per_kvbyte = 0 else: @@ -3052,7 +3059,7 @@ class LNWallet(LNWorker): feerate_per_kvbyte = self.network.fee_estimates.eta_target_to_fee(FEE_LN_LOW_ETA_TARGET) or 0 low_feerate_per_kw = max(FEERATE_PER_KW_MIN_RELAY_LIGHTNING, feerate_per_kvbyte // 4) # make sure this is never higher than the target feerate: - current_target_feerate = self.current_target_feerate_per_kw() + current_target_feerate = self.current_target_feerate_per_kw(has_anchors=False) if not current_target_feerate: return None low_feerate_per_kw = min(low_feerate_per_kw, current_target_feerate) diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py index b5624167f..159f747d4 100644 --- a/tests/test_lnpeer.py +++ b/tests/test_lnpeer.py @@ -339,7 +339,7 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): get_forwarding_failure = LNWallet.get_forwarding_failure maybe_cleanup_forwarding = LNWallet.maybe_cleanup_forwarding current_target_feerate_per_kw = LNWallet.current_target_feerate_per_kw - current_low_feerate_per_kw = LNWallet.current_low_feerate_per_kw + current_low_feerate_per_kw_srk_channel = LNWallet.current_low_feerate_per_kw_srk_channel maybe_cleanup_mpp = LNWallet.maybe_cleanup_mpp