Browse Source

modern shutdown:

- clarify TODOs
 - add tests for shutdown with modern negotiation
master
ThomasV 4 years ago
parent
commit
6667a79f10
  1. 54
      electrum/lnpeer.py
  2. 46
      electrum/tests/test_lnpeer.py

54
electrum/lnpeer.py

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

46
electrum/tests/test_lnpeer.py

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

Loading…
Cancel
Save