1
0

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:
f321x
2025-05-13 18:03:17 +02:00
parent 660ffa2b8f
commit 61874e9fe7
4 changed files with 66 additions and 33 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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