lnpeer: closing fee negociation:
- use fee_rate from config - set upper bound on fee - add test_close to test_lnpeer
This commit is contained in:
@@ -743,6 +743,9 @@ class Channel(Logger):
|
|||||||
def pending_local_fee(self):
|
def pending_local_fee(self):
|
||||||
return self.constraints.capacity - sum(x.value for x in self.get_next_commitment(LOCAL).outputs())
|
return self.constraints.capacity - sum(x.value for x in self.get_next_commitment(LOCAL).outputs())
|
||||||
|
|
||||||
|
def get_latest_fee(self, subject):
|
||||||
|
return self.constraints.capacity - sum(x.value for x in self.get_latest_commitment(subject).outputs())
|
||||||
|
|
||||||
def update_fee(self, feerate: int, from_us: bool):
|
def update_fee(self, feerate: int, from_us: bool):
|
||||||
# feerate uses sat/kw
|
# feerate uses sat/kw
|
||||||
if self.constraints.is_initiator != from_us:
|
if self.constraints.is_initiator != from_us:
|
||||||
|
|||||||
@@ -1381,20 +1381,33 @@ class Peer(Logger):
|
|||||||
while len(chan.hm.htlcs(LOCAL)) + len(chan.hm.htlcs(REMOTE)) > 0:
|
while len(chan.hm.htlcs(LOCAL)) + len(chan.hm.htlcs(REMOTE)) > 0:
|
||||||
self.logger.info(f'(chan: {chan.short_channel_id}) waiting for htlcs to settle...')
|
self.logger.info(f'(chan: {chan.short_channel_id}) waiting for htlcs to settle...')
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
our_fee = chan.pending_local_fee()
|
their_scriptpubkey = payload['scriptpubkey']
|
||||||
scriptpubkey = bfh(bitcoin.address_to_script(chan.sweep_address))
|
our_scriptpubkey = bfh(bitcoin.address_to_script(chan.sweep_address))
|
||||||
|
# estimate fee of closing tx
|
||||||
|
our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=0)
|
||||||
|
fee_rate = self.network.config.fee_per_kb()
|
||||||
|
our_fee = fee_rate * closing_tx.estimated_size() // 1000
|
||||||
|
# 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)
|
||||||
# negotiate fee
|
# negotiate fee
|
||||||
while True:
|
while True:
|
||||||
our_sig, closing_tx = chan.make_closing_tx(scriptpubkey, payload['scriptpubkey'], fee_sat=our_fee)
|
self.logger.info(f'sending closing_signed {our_fee}')
|
||||||
|
our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=our_fee)
|
||||||
self.send_message('closing_signed', channel_id=chan.channel_id, fee_satoshis=our_fee, signature=our_sig)
|
self.send_message('closing_signed', channel_id=chan.channel_id, fee_satoshis=our_fee, signature=our_sig)
|
||||||
# FIXME: the remote SHOULD send closing_signed, but some don't.
|
# FIXME: the remote SHOULD send closing_signed, but some don't.
|
||||||
cs_payload = await self.wait_for_message('closing_signed', chan.channel_id)
|
cs_payload = await self.wait_for_message('closing_signed', chan.channel_id)
|
||||||
their_fee = int.from_bytes(cs_payload['fee_satoshis'], 'big')
|
their_fee = int.from_bytes(cs_payload['fee_satoshis'], 'big')
|
||||||
their_sig = cs_payload['signature']
|
their_sig = cs_payload['signature']
|
||||||
if our_fee == their_fee:
|
# TODO: verify their sig
|
||||||
|
if their_fee > max_fee:
|
||||||
|
raise Exception(f'the proposed fee exceeds the base fee of the latest commitment transaction {is_local, their_fee, max_fee}')
|
||||||
|
# Agree if difference is lower or equal to one (see below)
|
||||||
|
if abs(our_fee - their_fee) < 2:
|
||||||
break
|
break
|
||||||
# TODO: negotiate better
|
# BOLT2: receiver MUST propose a value "strictly between" the received fee_satoshis and its previously-sent fee_satoshis.
|
||||||
our_fee = their_fee
|
our_fee = (our_fee + (1 if our_fee < their_fee else -1) + their_fee) // 2
|
||||||
|
|
||||||
# add signatures
|
# add signatures
|
||||||
closing_tx.add_signature_to_txin(txin_idx=0,
|
closing_tx.add_signature_to_txin(txin_idx=0,
|
||||||
signing_pubkey=chan.config[LOCAL].multisig_key.pubkey.hex(),
|
signing_pubkey=chan.config[LOCAL].multisig_key.pubkey.hex(),
|
||||||
@@ -1403,5 +1416,5 @@ class Peer(Logger):
|
|||||||
signing_pubkey=chan.config[REMOTE].multisig_key.pubkey.hex(),
|
signing_pubkey=chan.config[REMOTE].multisig_key.pubkey.hex(),
|
||||||
sig=bh2u(der_sig_from_sig_string(their_sig) + b'\x01'))
|
sig=bh2u(der_sig_from_sig_string(their_sig) + b'\x01'))
|
||||||
# broadcast
|
# broadcast
|
||||||
await self.network.broadcast_transaction(closing_tx)
|
await self.network.try_broadcasting(closing_tx, 'closing')
|
||||||
return closing_tx.txid()
|
return closing_tx.txid()
|
||||||
|
|||||||
@@ -165,6 +165,9 @@ def create_test_channels(feerate=6000, local=None, remote=None):
|
|||||||
alice.hm.channel_open_finished()
|
alice.hm.channel_open_finished()
|
||||||
bob.hm.channel_open_finished()
|
bob.hm.channel_open_finished()
|
||||||
|
|
||||||
|
# TODO: sweep_address in lnchannel.py should use static_remotekey
|
||||||
|
alice.sweep_address = bitcoin.pubkey_to_address('p2wpkh', alice.config[LOCAL].payment_basepoint.pubkey.hex())
|
||||||
|
bob.sweep_address = bitcoin.pubkey_to_address('p2wpkh', bob.config[LOCAL].payment_basepoint.pubkey.hex())
|
||||||
return alice, bob
|
return alice, bob
|
||||||
|
|
||||||
class TestFee(ElectrumTestCase):
|
class TestFee(ElectrumTestCase):
|
||||||
|
|||||||
@@ -283,6 +283,24 @@ class TestPeer(ElectrumTestCase):
|
|||||||
with self.assertRaises(concurrent.futures.CancelledError):
|
with self.assertRaises(concurrent.futures.CancelledError):
|
||||||
run(f())
|
run(f())
|
||||||
|
|
||||||
|
def test_close(self):
|
||||||
|
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('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', 1000)
|
||||||
|
async def pay():
|
||||||
|
await asyncio.wait_for(p1.initialized, 1)
|
||||||
|
await asyncio.wait_for(p2.initialized, 1)
|
||||||
|
await p1.close_channel(alice_channel.channel_id)
|
||||||
|
gath.cancel()
|
||||||
|
gath = asyncio.gather(pay(), p1._message_loop(), p2._message_loop())
|
||||||
|
async def f():
|
||||||
|
await gath
|
||||||
|
with self.assertRaises(concurrent.futures.CancelledError):
|
||||||
|
run(f())
|
||||||
|
|
||||||
def test_channel_usage_after_closing(self):
|
def test_channel_usage_after_closing(self):
|
||||||
alice_channel, bob_channel = create_test_channels()
|
alice_channel, bob_channel = create_test_channels()
|
||||||
p1, p2, w1, w2, q1, q2 = self.prepare_peers(alice_channel, bob_channel)
|
p1, p2, w1, w2, q1, q2 = self.prepare_peers(alice_channel, bob_channel)
|
||||||
|
|||||||
Reference in New Issue
Block a user