diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 820d9b656..055c14d8a 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -23,8 +23,8 @@ from .crypto import sha256, sha256d, privkey_to_pubkey from . import bitcoin, util from . import constants from .util import (bfh, log_exceptions, ignore_exceptions, chunks, OldTaskGroup, - UnrelatedTransactionException, error_text_bytes_to_safe_str, AsyncHangDetector) -from .util import event_listener, EventListener + UnrelatedTransactionException, error_text_bytes_to_safe_str, AsyncHangDetector, + NoDynamicFeeEstimates, event_listener, EventListener) from . import transaction from .bitcoin import make_op_return, DummyAddress from .transaction import PartialTxOutput, match_script_against_template, Sighash @@ -993,7 +993,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 = self.lnworker.current_target_feerate_per_kw() + feerate: Optional[int] = self.lnworker.current_target_feerate_per_kw() + if feerate is None: + raise NoDynamicFeeEstimates() # we set a channel type for internal bookkeeping open_channel_tlvs = {} assert self.their_features.supports(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT) @@ -1598,6 +1600,11 @@ class Peer(Logger, EventListener): # We should let them know: self._send_channel_reestablish(chan) 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: + # 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 if not (ChannelState.PREOPENING < chan.get_state() < ChannelState.FORCE_CLOSING): raise Exception(f"unexpected {chan.get_state()=} for reestablish") @@ -2590,12 +2597,16 @@ class Peer(Logger, EventListener): return if chan.get_state() != ChannelState.OPEN: return - feerate_per_kw = self.lnworker.current_target_feerate_per_kw() - def does_chan_fee_need_update(chan_feerate: Union[float, int]) -> bool: + feerate_per_kw: Optional[int] = self.lnworker.current_target_feerate_per_kw() + if feerate_per_kw is None: + return + 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: 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) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 05fa1105d..2ea3ea307 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -3000,21 +3000,28 @@ class LNWallet(LNWorker): else: await self.taskgroup.spawn(self.reestablish_peer_for_given_channel(chan)) - def current_target_feerate_per_kw(self) -> int: + def current_target_feerate_per_kw(self) -> Optional[int]: if self.network.fee_estimates.has_data(): feerate_per_kvbyte = self.network.fee_estimates.eta_target_to_fee(FEE_LN_ETA_TARGET) 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) -> int: + def current_low_feerate_per_kw(self) -> Optional[int]: if constants.net is constants.BitcoinRegtest: feerate_per_kvbyte = 0 else: + if not self.network.fee_estimates.has_data(): + return None 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: - low_feerate_per_kw = min(low_feerate_per_kw, self.current_target_feerate_per_kw()) + current_target_feerate = self.current_target_feerate_per_kw() + if not current_target_feerate: + return None + low_feerate_per_kw = min(low_feerate_per_kw, current_target_feerate) return low_feerate_per_kw def create_channel_backup(self, channel_id: bytes): diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py index 5113a82fc..b5624167f 100644 --- a/tests/test_lnpeer.py +++ b/tests/test_lnpeer.py @@ -44,8 +44,7 @@ from electrum.lnutil import LOCAL, REMOTE from electrum.invoices import PR_PAID, PR_UNPAID, Invoice from electrum.interface import GracefulDisconnect from electrum.simple_config import SimpleConfig -from electrum.fee_policy import FeeTimeEstimates - +from electrum.fee_policy import FeeTimeEstimates, FEE_ETA_TARGETS from .test_lnchannel import create_test_channels from .test_bitcoin import needs_test_with_all_chacha20_implementations @@ -69,6 +68,7 @@ class MockNetwork: self.lnwatcher = None self.interface = None self.fee_estimates = FeeTimeEstimates() + self.populate_fee_estimates() self.config = config self.asyncio_loop = util.get_asyncio_loop() self.channel_db = ChannelDB(self) @@ -95,6 +95,10 @@ class MockNetwork: async def try_broadcasting(self, tx, name): await self.broadcast_transaction(tx) + def populate_fee_estimates(self): + for target in FEE_ETA_TARGETS[:-1]: + self.fee_estimates.set_data(target, 50000 // target) + class MockBlockchain: @@ -144,6 +148,9 @@ class MockWallet: # note: sweep is not tested here, only in regtest return "tb1qqu5newtapamjchgxf0nty6geuykhvwas45q4q4" + def is_up_to_date(self): + return True + class MockLNGossip: def get_sync_progress_estimate(self):