Skip to content

Commit 456a921

Browse files
committed
lnpeer: modern fee negotiation
Updates the closing fee negotiation to comply with most recent spec changes, see lightning/bolts#847 The closing negotiation is backwards compatible with the old negotiation.
1 parent 772e828 commit 456a921

File tree

3 files changed

+145
-14
lines changed

3 files changed

+145
-14
lines changed

electrum/lnpeer.py

Lines changed: 130 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1770,28 +1770,67 @@ async def _shutdown(self, chan: Channel, payload, *, is_local: bool):
17701770
our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=0)
17711771
fee_rate = self.network.config.fee_per_kb()
17721772
our_fee = fee_rate * closing_tx.estimated_size() // 1000
1773+
# TODO: anchors: remove this, as commitment fee rate can be below chain head fee rate?
17731774
# BOLT2: The sending node MUST set fee less than or equal to the base fee of the final ctx
17741775
max_fee = chan.get_latest_fee(LOCAL if is_local else REMOTE)
17751776
our_fee = min(our_fee, max_fee)
1776-
drop_to_remote = False
1777+
1778+
drop_to_remote = False # does the peer drop its to_local output or not?
17771779
def send_closing_signed():
1780+
MODERN_FEE = True
1781+
if MODERN_FEE:
1782+
nonlocal fee_range_sent # we change fee_range_sent in outer scope
1783+
fee_range_sent = fee_range
1784+
closing_signed_tlvs = {'fee_range': fee_range}
1785+
else:
1786+
closing_signed_tlvs = {}
1787+
17781788
our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=our_fee, drop_remote=drop_to_remote)
1779-
self.send_message('closing_signed', channel_id=chan.channel_id, fee_satoshis=our_fee, signature=our_sig)
1789+
self.logger.info(f"Sending CLOSING_SIGNED with fee range: {closing_signed_tlvs} and fee: {our_fee}")
1790+
self.send_message(
1791+
'closing_signed',
1792+
channel_id=chan.channel_id,
1793+
fee_satoshis=our_fee,
1794+
signature=our_sig,
1795+
closing_signed_tlvs=closing_signed_tlvs,
1796+
)
17801797
def verify_signature(tx, sig):
17811798
their_pubkey = chan.config[REMOTE].multisig_key.pubkey
17821799
preimage_hex = tx.serialize_preimage(0)
17831800
pre_hash = sha256d(bfh(preimage_hex))
17841801
return ecc.verify_signature(their_pubkey, sig, pre_hash)
1802+
1803+
# this is the fee range we initially try to enforce
1804+
fee_range = {'min_fee_satoshis': our_fee // 2, 'max_fee_satoshis': our_fee} # TODO: find good lower bound
1805+
their_fee = None
1806+
fee_range_sent = {}
1807+
is_initiator = chan.constraints.is_initiator
17851808
# the funder sends the first 'closing_signed' message
1786-
if chan.constraints.is_initiator:
1809+
if is_initiator:
17871810
send_closing_signed()
1811+
17881812
# negotiate fee
17891813
while True:
1790-
# FIXME: the remote SHOULD send closing_signed, but some don't.
1791-
cs_payload = await self.wait_for_message('closing_signed', chan.channel_id)
1814+
try:
1815+
cs_payload = await self.wait_for_message('closing_signed', chan.channel_id)
1816+
except asyncio.exceptions.TimeoutError:
1817+
if not is_initiator and not their_fee: # we only force close if a peer doesn't reply
1818+
await self.lnworker.force_close_channel(chan.channel_id)
1819+
raise Exception("Peer didn't reply with closing signed, force closed.")
1820+
else:
1821+
# situation when we as an initiator send a fee and the recipient
1822+
# already agrees with that fee, but doens't tell us
1823+
raise Exception("Peer didn't reply, probably already closed.")
1824+
1825+
their_previous_fee = their_fee
17921826
their_fee = cs_payload['fee_satoshis']
1827+
1828+
# 0. integrity checks
1829+
# TODO: anchors, remove this?
17931830
if their_fee > max_fee:
17941831
raise Exception(f'the proposed fee exceeds the base fee of the latest commitment transaction {is_local, their_fee, max_fee}')
1832+
1833+
# determine their closing transaction
17951834
their_sig = cs_payload['signature']
17961835
# verify their sig: they might have dropped their output
17971836
our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=their_fee, drop_remote=False)
@@ -1812,17 +1851,94 @@ def verify_signature(tx, sig):
18121851
to_remote_amount = closing_tx.outputs()[to_remote_idx].value
18131852
transaction.check_scriptpubkey_template_and_dust(their_scriptpubkey, to_remote_amount)
18141853

1815-
# Agree if difference is lower or equal to one (see below)
1816-
if abs(our_fee - their_fee) < 2:
1854+
# 1. check fees
1855+
# if fee_satoshis is equal to its previously sent fee_satoshis:
1856+
if our_fee == their_fee:
1857+
# SHOULD sign and broadcast the final closing transaction.
1858+
break # we publish
1859+
1860+
# 2. at start, adapt our fee range if we are not the channel initiator
1861+
fee_range_received = cs_payload['closing_signed_tlvs'].get('fee_range')
1862+
if fee_range_received:
1863+
self.logger.info(f"Got CLOSING_SIGNED with fee range: {fee_range_received} and fee: {their_fee}")
1864+
# The sending node: if it is not the funder:
1865+
if fee_range_received and not is_initiator and not fee_range_sent:
1866+
# SHOULD set max_fee_satoshis to at least the max_fee_satoshis received
1867+
fee_range['max_fee_satoshis'] = max(fee_range_received['max_fee_satoshis'], fee_range['max_fee_satoshis'])
1868+
# SHOULD set min_fee_satoshis to a fairly low value
1869+
# TODO: what's fairly low value? allows the initiator to go to low values
1870+
fee_range['min_fee_satoshis'] = fee_range['max_fee_satoshis'] // 2
1871+
1872+
# 3. if fee_satoshis matches its previously sent fee_range:
1873+
if fee_range_sent and (fee_range_sent['min_fee_satoshis'] <= their_fee <= fee_range_sent['max_fee_satoshis']):
1874+
# SHOULD reply with a closing_signed with the same fee_satoshis value if it is different from its previously sent fee_satoshis
1875+
if our_fee != their_fee:
1876+
our_fee = their_fee
1877+
send_closing_signed() # peer publishes
1878+
break
1879+
# SHOULD use `fee_satoshis` to sign and broadcast the final closing transaction
1880+
else:
1881+
our_fee = their_fee
1882+
break # we publish
1883+
1884+
# 4. if the message contains a fee_range
1885+
if fee_range_received:
1886+
overlap_min = max(fee_range['min_fee_satoshis'], fee_range_received['min_fee_satoshis'])
1887+
overlap_max = min(fee_range['max_fee_satoshis'], fee_range_received['max_fee_satoshis'])
1888+
# if there is no overlap between that and its own fee_range
1889+
if overlap_min > overlap_max:
1890+
raise Exception("There is no overlap between between their and our fee range.")
1891+
# TODO: MUST fail the channel if it doesn't receive a satisfying fee_range after a reasonable amount of time
1892+
# otherwise:
1893+
else:
1894+
if is_initiator:
1895+
# if fee_satoshis is not in the overlap between the sent and received fee_range:
1896+
if not (overlap_min <= their_fee <= overlap_max):
1897+
# MUST fail the channel
1898+
await self.lnworker.force_close_channel(chan.channel_id)
1899+
raise Exception("Their fee is not in the overlap region, we force closed.")
1900+
# otherwise:
1901+
else:
1902+
our_fee = their_fee
1903+
# MUST reply with the same fee_satoshis.
1904+
send_closing_signed() # peer publishes
1905+
break
1906+
# otherwise (it is not the funder):
1907+
else:
1908+
# if it has already sent a closing_signed:
1909+
if fee_range_sent:
1910+
# if fee_satoshis is not the same as the value it sent:
1911+
if their_fee != our_fee:
1912+
# MUST fail the channel
1913+
await self.lnworker.force_close_channel(chan.channel_id)
1914+
raise Exception("Expected the same fee as ours, we force closed.")
1915+
# otherwise:
1916+
else:
1917+
# MUST propose a fee_satoshis in the overlap between received and (about-to-be) sent fee_range.
1918+
our_fee = (overlap_min + overlap_max) // 2
1919+
send_closing_signed()
1920+
continue
1921+
# otherwise, if fee_satoshis is not strictly between its last-sent fee_satoshis
1922+
# and its previously-received fee_satoshis, UNLESS it has since reconnected:
1923+
elif their_previous_fee and not (min(our_fee, their_previous_fee) < their_fee < max(our_fee, their_previous_fee)):
1924+
# SHOULD fail the connection.
1925+
raise Exception('Their fee is not between our last sent and their last sent fee.')
1926+
# otherwise, if the receiver agrees with the fee:
1927+
elif abs(their_fee - our_fee) <= 1: # we cannot have another strictly in-between value
1928+
# SHOULD reply with a closing_signed with the same fee_satoshis value.
18171929
our_fee = their_fee
1930+
send_closing_signed() # peer publishes
18181931
break
1819-
# this will be "strictly between" (as in BOLT2) previous values because of the above
1820-
our_fee = (our_fee + their_fee) // 2
1821-
# another round
1822-
send_closing_signed()
1823-
# the non-funder replies
1824-
if not chan.constraints.is_initiator:
1825-
send_closing_signed()
1932+
# otherwise:
1933+
else:
1934+
# MUST propose a value "strictly between" the received fee_satoshis and its previously-sent fee_satoshis.
1935+
our_fee_proposed = (our_fee + their_fee) // 2
1936+
if not (min(our_fee, their_fee) < our_fee_proposed < max(our_fee, their_fee)):
1937+
our_fee_proposed += (their_fee - our_fee) // 2
1938+
else:
1939+
our_fee = our_fee_proposed
1940+
send_closing_signed()
1941+
18261942
# add signatures
18271943
closing_tx.add_signature_to_txin(
18281944
txin_idx=0,

electrum/tests/regtest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ def test_unixsockets(self):
4444
class TestLightningAB(TestLightning):
4545
agents = ['alice', 'bob']
4646

47+
def test_collaborative_close(self):
48+
self.run_shell(['collaborative_close'])
49+
4750
def test_backup(self):
4851
self.run_shell(['backup'])
4952

electrum/tests/regtest/regtest.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,18 @@ if [[ $1 == "backup" ]]; then
158158
fi
159159

160160

161+
if [[ $1 == "collaborative_close" ]]; then
162+
wait_for_balance alice 1
163+
echo "alice opens channel"
164+
bob_node=$($bob nodeid)
165+
channel=$($alice open_channel $bob_node 0.15)
166+
new_blocks 3
167+
wait_until_channel_open alice
168+
echo "alice closes channel"
169+
request=$($bob close_channel $channel)
170+
fi
171+
172+
161173
if [[ $1 == "extract_preimage" ]]; then
162174
# instead of settling bob will broadcast
163175
$bob enable_htlc_settle false

0 commit comments

Comments
 (0)