fix: reduce update_fee target for anchor channels
the update_fee logic for lightning channels was not adapted to anchor channels causing us to send update_fee with a eta target of 2 blocks. This causes force closes when there are mempool spikes as the fees we try to update to are a lot higher than e.g. eclair uses. Eclair will force close if our fee is 10x > than their fee.
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user