mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-27 23:41:35 +00:00
ln: check if chain tip is stale when receiving HTLC
if so, don't release preimage / don't forward HTLC
This commit is contained in:
parent
12283d625b
commit
54e1520ee4
5 changed files with 58 additions and 20 deletions
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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…
Add table
Reference in a new issue