tlnpeer: add a few sanity checks to payment-forwarding (and related) - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit d2d4d19fcbf75ad5d64e9ef78c34d0bfc3b2164f DIR parent 0973b869251895fb796d98afcb877adad4c327b8 HTML Author: SomberNight <somber.night@protonmail.com> Date: Wed, 14 Aug 2019 21:41:24 +0200 lnpeer: add a few sanity checks to payment-forwarding (and related) Diffstat: M electrum/lnonion.py | 4 ++-- M electrum/lnpeer.py | 55 +++++++++++++++++++++++++------ 2 files changed, 47 insertions(+), 12 deletions(-) --- DIR diff --git a/electrum/lnonion.py b/electrum/lnonion.py t@@ -378,8 +378,8 @@ class OnionFailureCode(IntEnum): FEE_INSUFFICIENT = UPDATE | 12 INCORRECT_CLTV_EXPIRY = UPDATE | 13 EXPIRY_TOO_SOON = UPDATE | 14 - UNKNOWN_PAYMENT_HASH = PERM | 15 - INCORRECT_PAYMENT_AMOUNT = PERM | 16 + INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS = PERM | 15 + _LEGACY_INCORRECT_PAYMENT_AMOUNT = PERM | 16 FINAL_EXPIRY_TOO_SOON = 17 FINAL_INCORRECT_CLTV_EXPIRY = 18 FINAL_INCORRECT_HTLC_AMOUNT = 19 DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py t@@ -31,6 +31,7 @@ from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, ca process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage, ProcessedOnionPacket) from .lnchannel import Channel, RevokeAndAck, htlcsum, RemoteCtnTooFarInFuture +from . import lnutil from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc, RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, funding_output_script, get_per_commitment_secret_from_seed, t@@ -39,7 +40,8 @@ from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc, get_ln_flag_pair_of_bit, privkey_to_pubkey, UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_ACCEPTED, LightningPeerConnectionClosed, HandshakeFailed, NotFoundChanAnnouncementForUpdate, MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED, MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED, - MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED, RemoteMisbehaving, DEFAULT_TO_SELF_DELAY) + MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED, RemoteMisbehaving, DEFAULT_TO_SELF_DELAY, + NBLOCK_OUR_CLTV_EXPIRY_DELTA, format_short_channel_id) from .lnutil import FeeUpdate from .lntransport import LNTransport, LNTransportBase from .lnmsg import encode_msg, decode_msg t@@ -1110,6 +1112,8 @@ class Peer(Logger): async def await_remote(self, chan: Channel, ctn: int): self.maybe_send_commitment(chan) + # TODO review this. I suspect some callers want updates irrevocably committed, + # so comparision should use chan.get_oldest_unrevoked_ctn(REMOTE) while chan.get_latest_ctn(REMOTE) <= ctn: await self._remote_changed_events[chan.channel_id].wait() t@@ -1200,9 +1204,11 @@ class Peer(Logger): onion_packet = OnionPacket.from_bytes(payload["onion_routing_packet"]) processed_onion = process_onion_packet(onion_packet, associated_data=payment_hash, our_onion_private_key=self.privkey) chan = self.channels[channel_id] - assert chan.get_state() == "OPEN" + if chan.get_state() != "OPEN": + raise RemoteMisbehaving(f"received update_add_htlc while chan.get_state() != OPEN. state was {chan.get_state()}") if cltv_expiry >= 500_000_000: - pass # TODO fail the channel + self.lnworker.force_close_channel(channel_id) + raise RemoteMisbehaving(f"received update_add_htlc with cltv_expiry >= 500_000_000. value was {cltv_expiry}") # add htlc htlc = UpdateAddHtlc(amount_msat=amount_msat_htlc, payment_hash=payment_hash, t@@ -1243,15 +1249,44 @@ class Peer(Logger): return dph = processed_onion.hop_data.per_hop next_chan = self.lnworker.get_channel_by_short_id(dph.short_channel_id) + next_chan_scid = format_short_channel_id(dph.short_channel_id) next_peer = self.lnworker.peers[next_chan.node_id] - if next_chan is None or next_chan.get_state() != 'OPEN': - self.logger.info(f"cannot forward htlc {next_chan.get_state() if next_chan else None}") - reason = OnionRoutingFailureMessage(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'') + local_height = self.network.get_local_height() + if next_chan is None: + self.logger.info(f"cannot forward htlc. cannot find next_chan {next_chan_scid}") + reason = OnionRoutingFailureMessage(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'') + await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) + return + if next_chan.get_state() != 'OPEN': + self.logger.info(f"cannot forward htlc. next_chan not OPEN: {next_chan_scid} in state {next_chan.get_state()}") + #reason = OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=) # FIXME data + reason = OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'') await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) return - self.logger.info(f'forwarding htlc to {next_chan.node_id}') next_cltv_expiry = int.from_bytes(dph.outgoing_cltv_value, 'big') + if htlc.cltv_expiry - next_cltv_expiry < NBLOCK_OUR_CLTV_EXPIRY_DELTA: + #reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_CLTV_EXPIRY, data=) # FIXME data + reason = OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'') + await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) + return + if htlc.cltv_expiry - lnutil.NBLOCK_DEADLINE_BEFORE_EXPIRY_FOR_RECEIVED_HTLCS <= local_height \ + or next_cltv_expiry <= local_height: + #reason = OnionRoutingFailureMessage(code=OnionFailureCode.EXPIRY_TOO_SOON, data=) # FIXME data + reason = OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'') + await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) + return + if max(htlc.cltv_expiry, next_cltv_expiry) > local_height + lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE: + reason = OnionRoutingFailureMessage(code=OnionFailureCode.EXPIRY_TOO_FAR, data=b'') + await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) + return next_amount_msat_htlc = int.from_bytes(dph.amt_to_forward, 'big') + if htlc.amount_msat - next_amount_msat_htlc < 0: # TODO fees? + # reason = OnionRoutingFailureMessage(code=OnionFailureCode.FEE_INSUFFICIENT, data=) # FIXME data + reason = OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'') + await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) + return + + self.logger.info(f'forwarding htlc to {next_chan.node_id}') next_htlc = UpdateAddHtlc(amount_msat=next_amount_msat_htlc, payment_hash=htlc.payment_hash, cltv_expiry=next_cltv_expiry, timestamp=int(time.time())) next_htlc = next_chan.add_htlc(next_htlc) next_remote_ctn = next_chan.get_latest_ctn(REMOTE) t@@ -1280,13 +1315,13 @@ class Peer(Logger): invoice = self.lnworker.get_invoice(htlc.payment_hash) preimage = self.lnworker.get_preimage(htlc.payment_hash) except UnknownPaymentHash: - reason = OnionRoutingFailureMessage(code=OnionFailureCode.UNKNOWN_PAYMENT_HASH, data=b'') + reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'') await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) return expected_received_msat = int(invoice.amount * bitcoin.COIN * 1000) if invoice.amount is not None else None if expected_received_msat is not None and \ - (htlc.amount_msat < expected_received_msat or htlc.amount_msat > 2 * expected_received_msat): - reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_PAYMENT_AMOUNT, data=b'') + not (expected_received_msat <= htlc.amount_msat <= 2 * expected_received_msat): + reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'') await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) return local_height = self.network.get_local_height()