mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-28 16:01:30 +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.
|
# SOFTWARE.
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from typing import Optional, Dict, Mapping, Sequence
|
from typing import Optional, Dict, Mapping, Sequence
|
||||||
|
|
||||||
from . import util
|
from . import util
|
||||||
|
@ -484,6 +485,20 @@ class Blockchain(Logger):
|
||||||
height = self.height()
|
height = self.height()
|
||||||
return self.read_header(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 get_hash(self, height: int) -> str:
|
||||||
def is_height_checkpoint():
|
def is_height_checkpoint():
|
||||||
within_cp_range = height <= constants.net.max_checkpoint()
|
within_cp_range = height <= constants.net.max_checkpoint()
|
||||||
|
|
|
@ -1131,19 +1131,23 @@ class Peer(Logger):
|
||||||
chan.receive_htlc(htlc, onion_packet)
|
chan.receive_htlc(htlc, onion_packet)
|
||||||
|
|
||||||
def maybe_forward_htlc(self, chan: Channel, htlc: UpdateAddHtlc, *,
|
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
|
# Forward HTLC
|
||||||
# FIXME: there are critical safety checks MISSING here
|
# FIXME: there are critical safety checks MISSING here
|
||||||
forwarding_enabled = self.network.config.get('lightning_forward_payments', False)
|
forwarding_enabled = self.network.config.get('lightning_forward_payments', False)
|
||||||
if not forwarding_enabled:
|
if not forwarding_enabled:
|
||||||
self.logger.info(f"forwarding is disabled. failing htlc.")
|
self.logger.info(f"forwarding is disabled. failing htlc.")
|
||||||
return OnionRoutingFailureMessage(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'')
|
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:
|
try:
|
||||||
next_chan_scid = processed_onion.hop_data.payload["short_channel_id"]["short_channel_id"]
|
next_chan_scid = processed_onion.hop_data.payload["short_channel_id"]["short_channel_id"]
|
||||||
except:
|
except:
|
||||||
return OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
return OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
||||||
next_chan = self.lnworker.get_channel_by_short_id(next_chan_scid)
|
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:
|
if next_chan is None:
|
||||||
self.logger.info(f"cannot forward htlc. cannot find next_chan {next_chan_scid}")
|
self.logger.info(f"cannot forward htlc. cannot find next_chan {next_chan_scid}")
|
||||||
return OnionRoutingFailureMessage(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'')
|
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:
|
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
|
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)
|
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:
|
or next_cltv_expiry <= local_height:
|
||||||
data = outgoing_chan_upd_len + outgoing_chan_upd
|
data = outgoing_chan_upd_len + outgoing_chan_upd
|
||||||
return OnionRoutingFailureMessage(code=OnionFailureCode.EXPIRY_TOO_SOON, data=data)
|
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 OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=data)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def maybe_fulfill_htlc(self, chan: Channel, htlc: UpdateAddHtlc, *,
|
def maybe_fulfill_htlc(self, *, chan: Channel, htlc: UpdateAddHtlc,
|
||||||
onion_packet: OnionPacket, processed_onion: ProcessedOnionPacket):
|
onion_packet: OnionPacket, processed_onion: ProcessedOnionPacket,
|
||||||
|
) -> Tuple[Optional[bytes], Optional[OnionRoutingFailureMessage]]:
|
||||||
try:
|
try:
|
||||||
info = self.lnworker.get_payment_info(htlc.payment_hash)
|
info = self.lnworker.get_payment_info(htlc.payment_hash)
|
||||||
preimage = self.lnworker.get_preimage(htlc.payment_hash)
|
preimage = self.lnworker.get_preimage(htlc.payment_hash)
|
||||||
except UnknownPaymentHash:
|
except UnknownPaymentHash:
|
||||||
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
|
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
|
||||||
return False, reason
|
return None, reason
|
||||||
try:
|
try:
|
||||||
payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"]
|
payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"]
|
||||||
except:
|
except:
|
||||||
|
@ -1217,30 +1222,37 @@ class Peer(Logger):
|
||||||
else:
|
else:
|
||||||
if payment_secret_from_onion != derive_payment_secret_from_payment_preimage(preimage):
|
if payment_secret_from_onion != derive_payment_secret_from_payment_preimage(preimage):
|
||||||
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
|
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
|
expected_received_msat = int(info.amount * 1000) if info.amount is not None else None
|
||||||
if expected_received_msat is not None and \
|
if expected_received_msat is not None and \
|
||||||
not (expected_received_msat <= htlc.amount_msat <= 2 * expected_received_msat):
|
not (expected_received_msat <= htlc.amount_msat <= 2 * expected_received_msat):
|
||||||
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
|
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
|
||||||
return False, reason
|
return None, reason
|
||||||
local_height = self.network.get_local_height()
|
# 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:
|
if local_height + MIN_FINAL_CLTV_EXPIRY_ACCEPTED > htlc.cltv_expiry:
|
||||||
reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_EXPIRY_TOO_SOON, data=b'')
|
reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_EXPIRY_TOO_SOON, data=b'')
|
||||||
return False, reason
|
return None, reason
|
||||||
try:
|
try:
|
||||||
cltv_from_onion = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"]
|
cltv_from_onion = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"]
|
||||||
except:
|
except:
|
||||||
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
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:
|
if cltv_from_onion != htlc.cltv_expiry:
|
||||||
reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY,
|
reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY,
|
||||||
data=htlc.cltv_expiry.to_bytes(4, byteorder="big"))
|
data=htlc.cltv_expiry.to_bytes(4, byteorder="big"))
|
||||||
return False, reason
|
return None, reason
|
||||||
try:
|
try:
|
||||||
amount_from_onion = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"]
|
amount_from_onion = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"]
|
||||||
except:
|
except:
|
||||||
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
||||||
return False, reason
|
return None, reason
|
||||||
try:
|
try:
|
||||||
amount_from_onion = processed_onion.hop_data.payload["payment_data"]["total_msat"]
|
amount_from_onion = processed_onion.hop_data.payload["payment_data"]["total_msat"]
|
||||||
except:
|
except:
|
||||||
|
@ -1248,7 +1260,7 @@ class Peer(Logger):
|
||||||
if amount_from_onion > htlc.amount_msat:
|
if amount_from_onion > htlc.amount_msat:
|
||||||
reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT,
|
reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT,
|
||||||
data=htlc.amount_msat.to_bytes(8, byteorder="big"))
|
data=htlc.amount_msat.to_bytes(8, byteorder="big"))
|
||||||
return False, reason
|
return None, reason
|
||||||
# all good
|
# all good
|
||||||
return preimage, None
|
return preimage, None
|
||||||
|
|
||||||
|
|
|
@ -262,7 +262,8 @@ CHANNEL_OPENING_TIMEOUT = 24*60*60
|
||||||
##### CLTV-expiry-delta-related values
|
##### CLTV-expiry-delta-related values
|
||||||
# see https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#cltv_expiry_delta-selection
|
# 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
|
MIN_FINAL_CLTV_EXPIRY_ACCEPTED = 144
|
||||||
# set it a tiny bit higher for invoices as blocks could get mined
|
# set it a tiny bit higher for invoices as blocks could get mined
|
||||||
# during forward path of payment
|
# during forward path of payment
|
||||||
|
|
|
@ -58,6 +58,7 @@ class MockNetwork:
|
||||||
self.channel_db.data_loaded.set()
|
self.channel_db.data_loaded.set()
|
||||||
self.path_finder = LNPathFinder(self.channel_db)
|
self.path_finder = LNPathFinder(self.channel_db)
|
||||||
self.tx_queue = tx_queue
|
self.tx_queue = tx_queue
|
||||||
|
self._blockchain = MockBlockchain()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def callback_lock(self):
|
def callback_lock(self):
|
||||||
|
@ -70,6 +71,9 @@ class MockNetwork:
|
||||||
def get_local_height(self):
|
def get_local_height(self):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
def blockchain(self):
|
||||||
|
return self._blockchain
|
||||||
|
|
||||||
async def broadcast_transaction(self, tx):
|
async def broadcast_transaction(self, tx):
|
||||||
if self.tx_queue:
|
if self.tx_queue:
|
||||||
await self.tx_queue.put(tx)
|
await self.tx_queue.put(tx)
|
||||||
|
@ -77,6 +81,16 @@ class MockNetwork:
|
||||||
async def try_broadcasting(self, tx, name):
|
async def try_broadcasting(self, tx, name):
|
||||||
await self.broadcast_transaction(tx)
|
await self.broadcast_transaction(tx)
|
||||||
|
|
||||||
|
|
||||||
|
class MockBlockchain:
|
||||||
|
|
||||||
|
def height(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def is_tip_stale(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class MockWallet:
|
class MockWallet:
|
||||||
def set_label(self, x, y):
|
def set_label(self, x, y):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -174,11 +174,7 @@ def get_locktime_for_new_transaction(network: 'Network') -> int:
|
||||||
if not network:
|
if not network:
|
||||||
return 0
|
return 0
|
||||||
chain = network.blockchain()
|
chain = network.blockchain()
|
||||||
header = chain.header_at_tip()
|
if chain.is_tip_stale():
|
||||||
if not header:
|
|
||||||
return 0
|
|
||||||
STALE_DELAY = 8 * 60 * 60 # in seconds
|
|
||||||
if header['timestamp'] + STALE_DELAY < time.time():
|
|
||||||
return 0
|
return 0
|
||||||
# discourage "fee sniping"
|
# discourage "fee sniping"
|
||||||
locktime = chain.height()
|
locktime = chain.height()
|
||||||
|
|
Loading…
Add table
Reference in a new issue