commit 54e1520ee4cce041d46a011cdef3ba9d2d4ec043
parent 12283d625b49c3a7d70f5fa7e9246098b6caf6bc
Author: SomberNight <somber.night@protonmail.com>
Date: Mon, 13 Apr 2020 17:04:27 +0200
ln: check if chain tip is stale when receiving HTLC
if so, don't release preimage / don't forward HTLC
Diffstat:
5 files changed, 58 insertions(+), 20 deletions(-)
diff --git a/electrum/blockchain.py b/electrum/blockchain.py
@@ -22,6 +22,7 @@
# SOFTWARE.
import os
import threading
+import time
from typing import Optional, Dict, Mapping, Sequence
from . import util
@@ -484,6 +485,20 @@ class Blockchain(Logger):
height = self.height()
return self.read_header(height)
+ def is_tip_stale(self) -> bool:
+ STALE_DELAY = 8 * 60 * 60 # in seconds
+ header = self.header_at_tip()
+ if not header:
+ return True
+ # note: We check the timestamp only in the latest header.
+ # The Bitcoin consensus has a lot of leeway here:
+ # - needs to be greater than the median of the timestamps of the past 11 blocks, and
+ # - up to at most 2 hours into the future compared to local clock
+ # so there is ~2 hours of leeway in either direction
+ if header['timestamp'] + STALE_DELAY < time.time():
+ return True
+ return False
+
def get_hash(self, height: int) -> str:
def is_height_checkpoint():
within_cp_range = height <= constants.net.max_checkpoint()
diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
@@ -1131,19 +1131,23 @@ class Peer(Logger):
chan.receive_htlc(htlc, onion_packet)
def maybe_forward_htlc(self, chan: Channel, htlc: UpdateAddHtlc, *,
- onion_packet: OnionPacket, processed_onion: ProcessedOnionPacket):
+ onion_packet: OnionPacket, processed_onion: ProcessedOnionPacket
+ ) -> Optional[OnionRoutingFailureMessage]:
# Forward HTLC
# FIXME: there are critical safety checks MISSING here
forwarding_enabled = self.network.config.get('lightning_forward_payments', False)
if not forwarding_enabled:
self.logger.info(f"forwarding is disabled. failing htlc.")
return OnionRoutingFailureMessage(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'')
+ chain = self.network.blockchain()
+ if chain.is_tip_stale():
+ return OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
try:
next_chan_scid = processed_onion.hop_data.payload["short_channel_id"]["short_channel_id"]
except:
return OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
next_chan = self.lnworker.get_channel_by_short_id(next_chan_scid)
- local_height = self.network.get_local_height()
+ local_height = chain.height()
if next_chan is None:
self.logger.info(f"cannot forward htlc. cannot find next_chan {next_chan_scid}")
return OnionRoutingFailureMessage(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'')
@@ -1161,7 +1165,7 @@ class Peer(Logger):
if htlc.cltv_expiry - next_cltv_expiry < NBLOCK_OUR_CLTV_EXPIRY_DELTA:
data = htlc.cltv_expiry.to_bytes(4, byteorder="big") + outgoing_chan_upd_len + outgoing_chan_upd
return OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_CLTV_EXPIRY, data=data)
- if htlc.cltv_expiry - lnutil.NBLOCK_DEADLINE_BEFORE_EXPIRY_FOR_RECEIVED_HTLCS <= local_height \
+ if htlc.cltv_expiry - lnutil.MIN_FINAL_CLTV_EXPIRY_ACCEPTED <= local_height \
or next_cltv_expiry <= local_height:
data = outgoing_chan_upd_len + outgoing_chan_upd
return OnionRoutingFailureMessage(code=OnionFailureCode.EXPIRY_TOO_SOON, data=data)
@@ -1202,14 +1206,15 @@ class Peer(Logger):
return OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=data)
return None
- def maybe_fulfill_htlc(self, chan: Channel, htlc: UpdateAddHtlc, *,
- onion_packet: OnionPacket, processed_onion: ProcessedOnionPacket):
+ def maybe_fulfill_htlc(self, *, chan: Channel, htlc: UpdateAddHtlc,
+ onion_packet: OnionPacket, processed_onion: ProcessedOnionPacket,
+ ) -> Tuple[Optional[bytes], Optional[OnionRoutingFailureMessage]]:
try:
info = self.lnworker.get_payment_info(htlc.payment_hash)
preimage = self.lnworker.get_preimage(htlc.payment_hash)
except UnknownPaymentHash:
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
- return False, reason
+ return None, reason
try:
payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"]
except:
@@ -1217,30 +1222,37 @@ class Peer(Logger):
else:
if payment_secret_from_onion != derive_payment_secret_from_payment_preimage(preimage):
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
- return False, reason
+ return None, reason
expected_received_msat = int(info.amount * 1000) if info.amount is not None else None
if expected_received_msat is not None and \
not (expected_received_msat <= htlc.amount_msat <= 2 * expected_received_msat):
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
- return False, reason
- local_height = self.network.get_local_height()
+ return None, reason
+ # Check that our blockchain tip is sufficiently recent so that we have an approx idea of the height.
+ # We should not release the preimage for an HTLC that its sender could already time out as
+ # then they might try to force-close and it becomes a race.
+ chain = self.network.blockchain()
+ if chain.is_tip_stale():
+ reason = OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
+ return None, reason
+ local_height = chain.height()
if local_height + MIN_FINAL_CLTV_EXPIRY_ACCEPTED > htlc.cltv_expiry:
reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_EXPIRY_TOO_SOON, data=b'')
- return False, reason
+ return None, reason
try:
cltv_from_onion = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"]
except:
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
- return False, reason
+ return None, reason
if cltv_from_onion != htlc.cltv_expiry:
reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY,
data=htlc.cltv_expiry.to_bytes(4, byteorder="big"))
- return False, reason
+ return None, reason
try:
amount_from_onion = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"]
except:
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
- return False, reason
+ return None, reason
try:
amount_from_onion = processed_onion.hop_data.payload["payment_data"]["total_msat"]
except:
@@ -1248,7 +1260,7 @@ class Peer(Logger):
if amount_from_onion > htlc.amount_msat:
reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT,
data=htlc.amount_msat.to_bytes(8, byteorder="big"))
- return False, reason
+ return None, reason
# all good
return preimage, None
diff --git a/electrum/lnutil.py b/electrum/lnutil.py
@@ -262,7 +262,8 @@ CHANNEL_OPENING_TIMEOUT = 24*60*60
##### CLTV-expiry-delta-related values
# see https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#cltv_expiry_delta-selection
-# the minimum cltv_expiry accepted for terminal payments
+# the minimum cltv_expiry accepted for newly received HTLCs
+# note: when changing, consider Blockchain.is_tip_stale()
MIN_FINAL_CLTV_EXPIRY_ACCEPTED = 144
# set it a tiny bit higher for invoices as blocks could get mined
# during forward path of payment
diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py
@@ -58,6 +58,7 @@ class MockNetwork:
self.channel_db.data_loaded.set()
self.path_finder = LNPathFinder(self.channel_db)
self.tx_queue = tx_queue
+ self._blockchain = MockBlockchain()
@property
def callback_lock(self):
@@ -70,6 +71,9 @@ class MockNetwork:
def get_local_height(self):
return 0
+ def blockchain(self):
+ return self._blockchain
+
async def broadcast_transaction(self, tx):
if self.tx_queue:
await self.tx_queue.put(tx)
@@ -77,6 +81,16 @@ class MockNetwork:
async def try_broadcasting(self, tx, name):
await self.broadcast_transaction(tx)
+
+class MockBlockchain:
+
+ def height(self):
+ return 0
+
+ def is_tip_stale(self):
+ return False
+
+
class MockWallet:
def set_label(self, x, y):
pass
diff --git a/electrum/wallet.py b/electrum/wallet.py
@@ -174,11 +174,7 @@ def get_locktime_for_new_transaction(network: 'Network') -> int:
if not network:
return 0
chain = network.blockchain()
- header = chain.header_at_tip()
- if not header:
- return 0
- STALE_DELAY = 8 * 60 * 60 # in seconds
- if header['timestamp'] + STALE_DELAY < time.time():
+ if chain.is_tip_stale():
return 0
# discourage "fee sniping"
locktime = chain.height()