Browse Source

ln: check if chain tip is stale when receiving HTLC

if so, don't release preimage / don't forward HTLC
hard-fail-on-bad-server-string
SomberNight 5 years ago
parent
commit
54e1520ee4
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 15
      electrum/blockchain.py
  2. 40
      electrum/lnpeer.py
  3. 3
      electrum/lnutil.py
  4. 14
      electrum/tests/test_lnpeer.py
  5. 6
      electrum/wallet.py

15
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()

40
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

3
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

14
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

6
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()

Loading…
Cancel
Save