modern shutdown:
- clarify TODOs - add tests for shutdown with modern negotiation
This commit is contained in:
@@ -1825,6 +1825,31 @@ class Peer(Logger):
|
||||
# can fullfill or fail htlcs. cannot add htlcs, because state != OPEN
|
||||
chan.set_can_send_ctx_updates(True)
|
||||
|
||||
def get_shutdown_fee_range(self, chan, closing_tx, is_local):
|
||||
""" return the closing fee and fee range we initially try to enforce """
|
||||
config = self.network.config
|
||||
if config.get('test_shutdown_fee'):
|
||||
our_fee = config.get('test_shutdown_fee')
|
||||
else:
|
||||
fee_rate_per_kb = config.eta_target_to_fee(FEE_LN_ETA_TARGET)
|
||||
if not fee_rate_per_kb: # fallback
|
||||
fee_rate_per_kb = self.network.config.fee_per_kb()
|
||||
our_fee = fee_rate_per_kb * closing_tx.estimated_size() // 1000
|
||||
# TODO: anchors: remove this, as commitment fee rate can be below chain head fee rate?
|
||||
# BOLT2: The sending node MUST set fee less than or equal to the base fee of the final ctx
|
||||
max_fee = chan.get_latest_fee(LOCAL if is_local else REMOTE)
|
||||
our_fee = min(our_fee, max_fee)
|
||||
# config modern_fee_negotiation can be set in tests
|
||||
if config.get('test_shutdown_legacy'):
|
||||
our_fee_range = None
|
||||
elif config.get('test_shutdown_fee_range'):
|
||||
our_fee_range = config.get('test_shutdown_fee_range')
|
||||
else:
|
||||
# we aim at a fee between next block inclusion and some lower value
|
||||
our_fee_range = {'min_fee_satoshis': our_fee // 2, 'max_fee_satoshis': our_fee * 2}
|
||||
self.logger.info(f"Our fee range: {our_fee_range} and fee: {our_fee}")
|
||||
return our_fee, our_fee_range
|
||||
|
||||
@log_exceptions
|
||||
async def _shutdown(self, chan: Channel, payload, *, is_local: bool):
|
||||
# wait until no HTLCs remain in either commitment transaction
|
||||
@@ -1841,24 +1866,9 @@ class Peer(Logger):
|
||||
assert our_scriptpubkey
|
||||
# estimate fee of closing tx
|
||||
our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=0)
|
||||
fee_rate_per_kb = self.network.config.eta_target_to_fee(FEE_LN_ETA_TARGET)
|
||||
if not fee_rate_per_kb: # fallback
|
||||
fee_rate_per_kb = self.network.config.fee_per_kb()
|
||||
our_fee = fee_rate_per_kb * closing_tx.estimated_size() // 1000
|
||||
# TODO: anchors: remove this, as commitment fee rate can be below chain head fee rate?
|
||||
# BOLT2: The sending node MUST set fee less than or equal to the base fee of the final ctx
|
||||
max_fee = chan.get_latest_fee(LOCAL if is_local else REMOTE)
|
||||
our_fee = min(our_fee, max_fee)
|
||||
|
||||
is_initiator = chan.constraints.is_initiator
|
||||
# config modern_fee_negotiation can be set in tests
|
||||
if self.network.config.get('modern_fee_negotiation', True):
|
||||
# this is the fee range we initially try to enforce
|
||||
# we aim at a fee between next block inclusion and some lower value
|
||||
our_fee_range = {'min_fee_satoshis': our_fee // 2, 'max_fee_satoshis': our_fee * 2}
|
||||
self.logger.info(f"Our fee range: {our_fee_range} and fee: {our_fee}")
|
||||
else:
|
||||
our_fee_range = None
|
||||
our_fee, our_fee_range = self.get_shutdown_fee_range(chan, closing_tx, is_local)
|
||||
|
||||
def send_closing_signed(our_fee, our_fee_range, drop_remote):
|
||||
if our_fee_range:
|
||||
@@ -1916,12 +1926,14 @@ class Peer(Logger):
|
||||
fee_range_sent = our_fee_range and (is_initiator or (their_previous_fee is not None))
|
||||
|
||||
# The sending node, if it is not the funder:
|
||||
if our_fee_range and their_fee_range and not is_initiator:
|
||||
if our_fee_range and their_fee_range and not is_initiator and not self.network.config.get('test_shutdown_fee_range'):
|
||||
# SHOULD set max_fee_satoshis to at least the max_fee_satoshis received
|
||||
our_fee_range['max_fee_satoshis'] = max(their_fee_range['max_fee_satoshis'], our_fee_range['max_fee_satoshis'])
|
||||
# SHOULD set min_fee_satoshis to a fairly low value
|
||||
# TODO: what's fairly low value? allows the initiator to go to low values
|
||||
our_fee_range['min_fee_satoshis'] = our_fee_range['max_fee_satoshis'] // 2
|
||||
our_fee_range['min_fee_satoshis'] = min(their_fee_range['min_fee_satoshis'], our_fee_range['min_fee_satoshis'])
|
||||
# Note: the BOLT describes what the sending node SHOULD do.
|
||||
# However, this assumes that we have decided to send 'funding_signed' in response to their fee_range.
|
||||
# In practice, we might prefer to fail the channel in some cases (TODO)
|
||||
|
||||
# the receiving node, if fee_satoshis matches its previously sent fee_range,
|
||||
if fee_range_sent and (our_fee_range['min_fee_satoshis'] <= their_fee <= our_fee_range['max_fee_satoshis']):
|
||||
@@ -1934,7 +1946,9 @@ class Peer(Logger):
|
||||
overlap_max = min(our_fee_range['max_fee_satoshis'], their_fee_range['max_fee_satoshis'])
|
||||
# if there is no overlap between that and its own fee_range
|
||||
if overlap_min > overlap_max:
|
||||
# TODO: MUST fail the channel if it doesn't receive a satisfying fee_range after a reasonable amount of time
|
||||
# TODO: the receiving node should first send a warning, and fail the channel
|
||||
# only if it doesn't receive a satisfying fee_range after a reasonable amount of time
|
||||
self.lnworker.schedule_force_closing(chan.channel_id)
|
||||
raise Exception("There is no overlap between between their and our fee range.")
|
||||
# otherwise, if it is the funder
|
||||
if is_initiator:
|
||||
|
||||
@@ -1063,22 +1063,46 @@ class TestPeer(TestCaseForTestnet):
|
||||
run(f())
|
||||
|
||||
@needs_test_with_all_chacha20_implementations
|
||||
def test_close_modern(self):
|
||||
self._test_close(True, True)
|
||||
def test_legacy_shutdown_low(self):
|
||||
self._test_shutdown(alice_fee=100, bob_fee=150)
|
||||
|
||||
@needs_test_with_all_chacha20_implementations
|
||||
def test_close_old_style(self):
|
||||
self._test_close(False, False)
|
||||
def test_legacy_shutdown_high(self):
|
||||
self._test_shutdown(alice_fee=2000, bob_fee=100)
|
||||
|
||||
def _test_close(self, modern_alice, modern_bob):
|
||||
@needs_test_with_all_chacha20_implementations
|
||||
def test_modern_shutdown_with_overlap(self):
|
||||
self._test_shutdown(
|
||||
alice_fee=1,
|
||||
bob_fee=200,
|
||||
alice_fee_range={'min_fee_satoshis': 1, 'max_fee_satoshis': 10},
|
||||
bob_fee_range={'min_fee_satoshis': 10, 'max_fee_satoshis': 300})
|
||||
|
||||
## This test works but it is too slow (LN_P2P_NETWORK_TIMEOUT)
|
||||
## because tests do not use a proper LNWorker object
|
||||
#@needs_test_with_all_chacha20_implementations
|
||||
#def test_modern_shutdown_no_overlap(self):
|
||||
# self.assertRaises(Exception, lambda: asyncio.run(
|
||||
# self._test_shutdown(
|
||||
# alice_fee=1,
|
||||
# bob_fee=200,
|
||||
# alice_fee_range={'min_fee_satoshis': 1, 'max_fee_satoshis': 10},
|
||||
# bob_fee_range={'min_fee_satoshis': 50, 'max_fee_satoshis': 300})
|
||||
# ))
|
||||
|
||||
def _test_shutdown(self, alice_fee, bob_fee, alice_fee_range=None, bob_fee_range=None):
|
||||
alice_channel, bob_channel = create_test_channels()
|
||||
p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel)
|
||||
w1.network.config.set_key('modern_fee_negotiation', modern_alice)
|
||||
w2.network.config.set_key('modern_fee_negotiation', modern_bob)
|
||||
w1.network.config.set_key('dynamic_fees', False)
|
||||
w2.network.config.set_key('dynamic_fees', False)
|
||||
w1.network.config.set_key('fee_per_kb', 5000)
|
||||
w2.network.config.set_key('fee_per_kb', 2000)
|
||||
w1.network.config.set_key('test_shutdown_fee', alice_fee)
|
||||
w2.network.config.set_key('test_shutdown_fee', bob_fee)
|
||||
if alice_fee_range is not None:
|
||||
w1.network.config.set_key('test_shutdown_fee_range', alice_fee_range)
|
||||
else:
|
||||
w1.network.config.set_key('test_shutdown_legacy', True)
|
||||
if bob_fee_range is not None:
|
||||
w2.network.config.set_key('test_shutdown_fee_range', bob_fee_range)
|
||||
else:
|
||||
w2.network.config.set_key('test_shutdown_legacy', True)
|
||||
w2.enable_htlc_settle = False
|
||||
lnaddr, pay_req = run(self.prepare_invoice(w2))
|
||||
async def pay():
|
||||
|
||||
Reference in New Issue
Block a user